642 lines
16 KiB
TypeScript
642 lines
16 KiB
TypeScript
import {
|
|
assertWorkspaceValidity,
|
|
createAffectedMetadata,
|
|
getImplicitDependencies,
|
|
getProjectNodes,
|
|
NxJson,
|
|
ProjectNode,
|
|
ProjectType
|
|
} from './shared';
|
|
import { DependencyType, Deps } from './deps-calculator';
|
|
|
|
describe('assertWorkspaceValidity', () => {
|
|
let mockNxJson;
|
|
let mockWorkspaceJson;
|
|
|
|
beforeEach(() => {
|
|
mockNxJson = {
|
|
projects: {
|
|
app1: {
|
|
tags: []
|
|
},
|
|
'app1-e2e': {
|
|
tags: []
|
|
},
|
|
app2: {
|
|
tags: []
|
|
},
|
|
'app2-e2e': {
|
|
tags: []
|
|
},
|
|
lib1: {
|
|
tags: []
|
|
},
|
|
lib2: {
|
|
tags: []
|
|
}
|
|
}
|
|
};
|
|
mockWorkspaceJson = {
|
|
projects: {
|
|
app1: {},
|
|
'app1-e2e': {},
|
|
app2: {},
|
|
'app2-e2e': {},
|
|
lib1: {},
|
|
lib2: {}
|
|
}
|
|
};
|
|
});
|
|
|
|
it('should not throw for a valid workspace', () => {
|
|
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
|
});
|
|
|
|
it('should throw for a missing project in workspace.json', () => {
|
|
delete mockWorkspaceJson.projects.app1;
|
|
try {
|
|
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
|
fail('Did not throw');
|
|
} catch (e) {
|
|
expect(e.message).toContain('projects are missing in');
|
|
}
|
|
});
|
|
|
|
it('should throw for a missing project in nx.json', () => {
|
|
delete mockNxJson.projects.app1;
|
|
try {
|
|
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
|
fail('Did not throw');
|
|
} catch (e) {
|
|
expect(e.message).toContain('projects are missing in nx.json');
|
|
}
|
|
});
|
|
|
|
it('should throw for an invalid top-level implicit dependency', () => {
|
|
mockNxJson.implicitDependencies = {
|
|
'README.md': ['invalidproj']
|
|
};
|
|
try {
|
|
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
|
fail('Did not throw');
|
|
} catch (e) {
|
|
expect(e.message).toContain(
|
|
'implicitDependencies specified in nx.json are invalid'
|
|
);
|
|
expect(e.message).toContain(' README.md');
|
|
expect(e.message).toContain(' invalidproj');
|
|
}
|
|
});
|
|
|
|
it('should throw for an invalid project-level implicit dependency', () => {
|
|
mockNxJson.projects.app2.implicitDependencies = ['invalidproj'];
|
|
|
|
try {
|
|
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
|
fail('Did not throw');
|
|
} catch (e) {
|
|
expect(e.message).toContain(
|
|
'implicitDependencies specified in nx.json are invalid'
|
|
);
|
|
expect(e.message).toContain(' app2');
|
|
expect(e.message).toContain(' invalidproj');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('getImplicitDependencies', () => {
|
|
let mockNxJson: NxJson;
|
|
let mockworkspaceJson: any;
|
|
|
|
beforeEach(() => {
|
|
mockNxJson = {
|
|
npmScope: 'proj',
|
|
projects: {
|
|
app1: {
|
|
tags: []
|
|
},
|
|
'app1-e2e': {
|
|
tags: []
|
|
},
|
|
app2: {
|
|
tags: []
|
|
},
|
|
'app2-e2e': {
|
|
tags: []
|
|
},
|
|
lib1: {
|
|
tags: []
|
|
},
|
|
lib2: {
|
|
tags: []
|
|
}
|
|
}
|
|
};
|
|
mockworkspaceJson = {
|
|
projects: {
|
|
app1: {
|
|
projectType: 'application'
|
|
},
|
|
'app1-e2e': {
|
|
projectType: 'application'
|
|
},
|
|
app2: {
|
|
projectType: 'application'
|
|
},
|
|
'app2-e2e': {
|
|
projectType: 'application'
|
|
},
|
|
lib1: {
|
|
projectType: 'library'
|
|
},
|
|
lib2: {
|
|
projectType: 'library'
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
describe('top-level implicit dependencies', () => {
|
|
it('should return implicit dependencies', () => {
|
|
mockNxJson.implicitDependencies = {
|
|
Jenkinsfile: ['app1', 'app2']
|
|
};
|
|
|
|
const result = getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
files: {
|
|
Jenkinsfile: ['app1', 'app2']
|
|
},
|
|
projects: {
|
|
app1: ['app1-e2e'],
|
|
app2: ['app2-e2e']
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should normalize wildcards into all projects', () => {
|
|
mockNxJson.implicitDependencies = {
|
|
'package.json': '*'
|
|
};
|
|
|
|
const result = getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
files: {
|
|
'package.json': [
|
|
'app1',
|
|
'app1-e2e',
|
|
'app2',
|
|
'app2-e2e',
|
|
'lib1',
|
|
'lib2'
|
|
]
|
|
},
|
|
projects: {
|
|
app1: ['app1-e2e'],
|
|
app2: ['app2-e2e']
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should call throw for an invalid workspace', () => {
|
|
delete mockNxJson.projects.app1;
|
|
try {
|
|
getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
fail('did not throw');
|
|
} catch (e) {}
|
|
});
|
|
});
|
|
|
|
describe('project-based implicit dependencies', () => {
|
|
it('should default appropriately', () => {
|
|
const result = getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
files: {},
|
|
projects: {
|
|
app1: ['app1-e2e'],
|
|
app2: ['app2-e2e']
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should allow setting on libs and apps', () => {
|
|
mockNxJson.projects.app2.implicitDependencies = ['app1'];
|
|
mockNxJson.projects.lib2.implicitDependencies = ['lib1'];
|
|
|
|
const result = getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
files: {},
|
|
projects: {
|
|
app1: ['app1-e2e', 'app2'],
|
|
app2: ['app2-e2e'],
|
|
lib1: ['lib2']
|
|
}
|
|
});
|
|
});
|
|
|
|
// NOTE: originally e2e apps had a magic dependency on their target app by naming convention.
|
|
// So, 'appName-e2e' depended on 'appName'.
|
|
it('should override magic e2e dependencies if specified', () => {
|
|
mockNxJson.projects['app1-e2e'].implicitDependencies = ['app2'];
|
|
|
|
const result = getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
files: {},
|
|
projects: {
|
|
app2: ['app1-e2e', 'app2-e2e']
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should fallback to magic e2e dependencies if empty array specified', () => {
|
|
mockNxJson.projects['app1-e2e'].implicitDependencies = [];
|
|
|
|
const result = getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
files: {},
|
|
projects: {
|
|
app1: ['app1-e2e'],
|
|
app2: ['app2-e2e']
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('project-based and top-level implicit dependencies', () => {
|
|
it('allows setting both', () => {
|
|
mockNxJson.implicitDependencies = {
|
|
Jenkinsfile: ['app1', 'app2']
|
|
};
|
|
mockNxJson.projects.app2.implicitDependencies = ['app1'];
|
|
|
|
const result = getImplicitDependencies(
|
|
getProjectNodes(mockworkspaceJson, mockNxJson),
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
);
|
|
|
|
expect(result).toEqual({
|
|
files: {
|
|
Jenkinsfile: ['app1', 'app2']
|
|
},
|
|
projects: {
|
|
app1: ['app1-e2e', 'app2'],
|
|
app2: ['app2-e2e']
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getProjectNodes', () => {
|
|
let mockNxJson;
|
|
let mockworkspaceJson;
|
|
|
|
beforeEach(() => {
|
|
mockNxJson = {
|
|
projects: {
|
|
app1: {
|
|
tags: []
|
|
},
|
|
'app1-e2e': {
|
|
tags: []
|
|
},
|
|
'customName-e2e': {
|
|
tags: []
|
|
},
|
|
lib1: {
|
|
tags: []
|
|
},
|
|
lib2: {
|
|
tags: []
|
|
}
|
|
}
|
|
};
|
|
mockworkspaceJson = {
|
|
projects: {
|
|
app1: {
|
|
projectType: 'application'
|
|
},
|
|
'app1-e2e': {
|
|
projectType: 'application'
|
|
},
|
|
'customName-e2e': {
|
|
projectType: 'application'
|
|
},
|
|
lib1: {
|
|
projectType: 'library'
|
|
},
|
|
lib2: {
|
|
projectType: 'library'
|
|
}
|
|
}
|
|
};
|
|
});
|
|
|
|
it('should parse nodes as correct type', () => {
|
|
const result: Pick<ProjectNode, 'name' | 'type'>[] = getProjectNodes(
|
|
mockworkspaceJson,
|
|
mockNxJson
|
|
).map(node => {
|
|
return { name: node.name, type: node.type };
|
|
});
|
|
expect(result).toEqual([
|
|
{
|
|
name: 'app1',
|
|
type: ProjectType.app
|
|
},
|
|
{
|
|
name: 'app1-e2e',
|
|
type: ProjectType.e2e
|
|
},
|
|
{
|
|
name: 'customName-e2e',
|
|
type: ProjectType.e2e
|
|
},
|
|
{
|
|
name: 'lib1',
|
|
type: ProjectType.lib
|
|
},
|
|
{
|
|
name: 'lib2',
|
|
type: ProjectType.lib
|
|
}
|
|
]);
|
|
});
|
|
|
|
it('should normalize missing architect configurations to an empty object', () => {
|
|
const result = getProjectNodes(mockworkspaceJson, mockNxJson).map(node => {
|
|
return { name: node.name, architect: node.architect };
|
|
});
|
|
expect(result).toEqual([
|
|
{
|
|
name: 'app1',
|
|
architect: {}
|
|
},
|
|
{
|
|
name: 'app1-e2e',
|
|
architect: {}
|
|
},
|
|
{
|
|
name: 'customName-e2e',
|
|
architect: {}
|
|
},
|
|
{
|
|
name: 'lib1',
|
|
architect: {}
|
|
},
|
|
{
|
|
name: 'lib2',
|
|
architect: {}
|
|
}
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('createAffectedMetadata', () => {
|
|
let projectNodes: Partial<ProjectNode>[];
|
|
let dependencies: Deps;
|
|
|
|
beforeEach(() => {
|
|
projectNodes = [
|
|
{
|
|
name: 'app1'
|
|
},
|
|
{
|
|
name: 'app2'
|
|
},
|
|
{
|
|
name: 'app1-e2e'
|
|
},
|
|
{
|
|
name: 'customName-e2e'
|
|
},
|
|
{
|
|
name: 'lib1'
|
|
},
|
|
{
|
|
name: 'lib2'
|
|
}
|
|
];
|
|
dependencies = {
|
|
'app1-e2e': [
|
|
{
|
|
projectName: 'app1',
|
|
type: DependencyType.implicit
|
|
}
|
|
],
|
|
'customName-e2e': [
|
|
{
|
|
projectName: 'app2',
|
|
type: DependencyType.implicit
|
|
}
|
|
],
|
|
app1: [
|
|
{
|
|
projectName: 'lib1',
|
|
type: DependencyType.es6Import
|
|
}
|
|
],
|
|
app2: [
|
|
{
|
|
projectName: 'lib1',
|
|
type: DependencyType.es6Import
|
|
},
|
|
{
|
|
projectName: 'lib2',
|
|
type: DependencyType.es6Import
|
|
}
|
|
],
|
|
lib1: [],
|
|
lib2: []
|
|
};
|
|
});
|
|
|
|
it('should translate project nodes array to map', () => {
|
|
expect(
|
|
createAffectedMetadata(projectNodes as ProjectNode[], dependencies, [])
|
|
.dependencyGraph.projects
|
|
).toEqual({
|
|
app1: {
|
|
name: 'app1'
|
|
},
|
|
app2: {
|
|
name: 'app2'
|
|
},
|
|
'app1-e2e': {
|
|
name: 'app1-e2e'
|
|
},
|
|
'customName-e2e': {
|
|
name: 'customName-e2e'
|
|
},
|
|
lib1: {
|
|
name: 'lib1'
|
|
},
|
|
lib2: {
|
|
name: 'lib2'
|
|
}
|
|
});
|
|
});
|
|
|
|
it('should include the dependencies', () => {
|
|
expect(
|
|
createAffectedMetadata(projectNodes as ProjectNode[], dependencies, [])
|
|
.dependencyGraph.dependencies
|
|
).toEqual(dependencies);
|
|
});
|
|
|
|
it('should find the roots', () => {
|
|
expect(
|
|
createAffectedMetadata(projectNodes as ProjectNode[], dependencies, [])
|
|
.dependencyGraph.roots
|
|
).toEqual(['app1-e2e', 'customName-e2e']);
|
|
});
|
|
|
|
it('should set projects as touched', () => {
|
|
const { projectStates } = createAffectedMetadata(
|
|
projectNodes as ProjectNode[],
|
|
dependencies,
|
|
['app1', 'lib2']
|
|
);
|
|
expect(projectStates.app1.touched).toEqual(true);
|
|
expect(projectStates.lib2.touched).toEqual(true);
|
|
|
|
expect(projectStates.lib1.touched).toEqual(false);
|
|
expect(projectStates.app2.touched).toEqual(false);
|
|
expect(projectStates['customName-e2e'].touched).toEqual(false);
|
|
expect(projectStates['app1-e2e'].touched).toEqual(false);
|
|
});
|
|
|
|
it('should set touched projects as affected', () => {
|
|
const { projectStates } = createAffectedMetadata(
|
|
projectNodes as ProjectNode[],
|
|
dependencies,
|
|
['app1', 'lib2']
|
|
);
|
|
expect(projectStates.app1.affected).toEqual(true);
|
|
expect(projectStates.lib2.affected).toEqual(true);
|
|
});
|
|
|
|
it('should set dependents of touched projects as affected', () => {
|
|
const { projectStates } = createAffectedMetadata(
|
|
projectNodes as ProjectNode[],
|
|
dependencies,
|
|
['app1']
|
|
);
|
|
expect(projectStates.app1.affected).toEqual(true);
|
|
expect(projectStates['app1-e2e'].affected).toEqual(true);
|
|
|
|
expect(projectStates.lib1.affected).toEqual(false);
|
|
expect(projectStates.lib2.affected).toEqual(false);
|
|
expect(projectStates.app2.affected).toEqual(false);
|
|
expect(projectStates['customName-e2e'].affected).toEqual(false);
|
|
});
|
|
|
|
it('should set dependents of touched projects as affected (2)', () => {
|
|
const { projectStates } = createAffectedMetadata(
|
|
projectNodes as ProjectNode[],
|
|
dependencies,
|
|
['lib1']
|
|
);
|
|
expect(projectStates.app1.affected).toEqual(true);
|
|
expect(projectStates['app1-e2e'].affected).toEqual(true);
|
|
expect(projectStates.lib1.affected).toEqual(true);
|
|
expect(projectStates.app2.affected).toEqual(true);
|
|
expect(projectStates['customName-e2e'].affected).toEqual(true);
|
|
|
|
expect(projectStates.lib2.affected).toEqual(false);
|
|
});
|
|
|
|
it('should not set any projects as affected when none are touched', () => {
|
|
const { projectStates } = createAffectedMetadata(
|
|
projectNodes as ProjectNode[],
|
|
dependencies,
|
|
[]
|
|
);
|
|
expect(projectStates.app1.affected).toEqual(false);
|
|
expect(projectStates.app2.affected).toEqual(false);
|
|
expect(projectStates.lib1.affected).toEqual(false);
|
|
expect(projectStates.lib2.affected).toEqual(false);
|
|
expect(projectStates['app1-e2e'].affected).toEqual(false);
|
|
expect(projectStates['customName-e2e'].affected).toEqual(false);
|
|
});
|
|
|
|
it('should handle circular dependencies', () => {
|
|
dependencies['lib2'].push({
|
|
projectName: 'app2',
|
|
type: DependencyType.es6Import
|
|
});
|
|
const metadata = createAffectedMetadata(
|
|
projectNodes as ProjectNode[],
|
|
dependencies,
|
|
['lib2']
|
|
);
|
|
const { dependencyGraph, projectStates } = metadata;
|
|
expect(dependencyGraph.roots).toEqual(['app1-e2e', 'customName-e2e']);
|
|
expect(projectStates.app1.affected).toEqual(false);
|
|
expect(projectStates.app2.affected).toEqual(true);
|
|
expect(projectStates.lib1.affected).toEqual(false);
|
|
expect(projectStates.lib2.affected).toEqual(true);
|
|
expect(projectStates['app1-e2e'].affected).toEqual(false);
|
|
expect(projectStates['customName-e2e'].affected).toEqual(true);
|
|
});
|
|
|
|
it('should cases where there is no root', () => {
|
|
dependencies['lib1'].push({
|
|
projectName: 'app1-e2e',
|
|
type: DependencyType.es6Import
|
|
});
|
|
dependencies['lib2'].push({
|
|
projectName: 'customName-e2e',
|
|
type: DependencyType.es6Import
|
|
});
|
|
const metadata = createAffectedMetadata(
|
|
projectNodes as ProjectNode[],
|
|
dependencies,
|
|
['app1-e2e', 'customName-e2e']
|
|
);
|
|
const { dependencyGraph, projectStates } = metadata;
|
|
expect(dependencyGraph.roots).toEqual([]);
|
|
expect(projectStates.app1.affected).toEqual(true);
|
|
expect(projectStates.app2.affected).toEqual(true);
|
|
expect(projectStates.lib1.affected).toEqual(true);
|
|
expect(projectStates.lib2.affected).toEqual(true);
|
|
expect(projectStates['app1-e2e'].affected).toEqual(true);
|
|
expect(projectStates['customName-e2e'].affected).toEqual(true);
|
|
});
|
|
});
|