import * as fs from 'fs'; import { DepsCalculator, DependencyType, NxDepsJson, dependencies } from './deps-calculator'; import { ProjectType, ProjectNode } from './affected-apps'; import { serializeJson } from '../utils/fileutils'; describe('DepsCalculator', () => { let depsCalculator: DepsCalculator; let initialDeps: NxDepsJson; let virtualFs: { [key: string]: string; }; let projects: ProjectNode[]; let fileRead: (path: string) => string; beforeEach(() => { initialDeps = { dependencies: {}, files: {} }; virtualFs = {}; projects = [ { name: 'app1Name', root: 'apps/app1', files: ['app1.ts'], fileMTimes: { 'app1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.ts'], fileMTimes: { 'lib1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.ts'], fileMTimes: { 'lib2.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ]; fileRead = file => { switch (file) { case 'dist/nxdeps.json': return serializeJson(initialDeps); default: return virtualFs[file]; } }; }); describe('initialization', () => { it('should not be incremental for new graphs', () => { depsCalculator = new DepsCalculator('nrwl', projects, null, fileRead); expect(depsCalculator.getDeps()).toEqual({ lib2Name: [], lib1Name: [], app1Name: [] }); expect(depsCalculator.incrementalEnabled).toEqual(false); }); it('should be incremental for an existing graph with no projects added or removed', () => { initialDeps.dependencies = { app1Name: [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], lib1Name: [], lib2Name: [] }; initialDeps.files = { 'app1.ts': [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], 'lib1.ts': [], 'lib2.ts': [] }; const result = new DepsCalculator( 'nrwl', projects, initialDeps, fileRead ); expect(result.incrementalEnabled).toEqual(true); expect(result.getDeps()).toEqual({ lib2Name: [], lib1Name: [], app1Name: [ { projectName: 'lib1Name', type: 'loadChildren' }, { projectName: 'lib2Name', type: 'loadChildren' } ] }); }); it('should not be incremental if projects are added to an existing graph', () => { initialDeps.dependencies = { app1Name: [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], lib1Name: [], lib2Name: [] }; initialDeps.files = { 'app1.ts': [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], 'lib1.ts': [], 'lib2.ts': [] }; projects = [ ...projects, { name: 'lib3Name', root: 'libs/lib3', files: ['lib3.ts'], fileMTimes: { 'lib3.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ]; const result = new DepsCalculator( 'nrwl', projects, initialDeps, fileRead ); expect(result.incrementalEnabled).toEqual(false); expect(result.getDeps()).toEqual({ app1Name: [], lib1Name: [], lib2Name: [], lib3Name: [] }); }); it('should not be incremental if projects are removed from an existing graph', () => { initialDeps.dependencies = { app1Name: [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], lib1Name: [], lib2Name: [] }; initialDeps.files = { 'app1.ts': [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], 'lib1.ts': [], 'lib2.ts': [] }; projects = projects.filter(p => p.name !== 'lib2Name'); const result = new DepsCalculator( 'nrwl', projects, initialDeps, fileRead ); expect(result.incrementalEnabled).toEqual(false); expect(result.getDeps()).toEqual({ lib1Name: [], app1Name: [] }); }); it('should not be incremental if projects are renamed in an existing graph', () => { initialDeps.dependencies = { app1Name: [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], lib1Name: [], lib2Name: [] }; initialDeps.files = { 'app1.ts': [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], 'lib1.ts': [], 'lib2.ts': [] }; projects = projects.map(proj => { if (proj.name !== 'app1Name') { return proj; } return { ...proj, name: 'newApp1Name' }; }); const result = new DepsCalculator( 'nrwl', projects, initialDeps, fileRead ); expect(result.incrementalEnabled).toEqual(false); expect(result.getDeps()).toEqual({ newApp1Name: [], lib1Name: [], lib2Name: [] }); }); it('should not be incremental if a legacy existing dependencies exists', () => { delete initialDeps.dependencies; delete initialDeps.files; const result = new DepsCalculator( 'nrwl', projects, initialDeps, fileRead ); expect(result.incrementalEnabled).toEqual(false); expect(result.getDeps()).toEqual({ app1Name: [], lib1Name: [], lib2Name: [] }); }); }); describe('incremental', () => { beforeEach(() => { virtualFs = { 'app1.ts': ` const routes = { path: 'a', loadChildren: '@nrwl/lib1#LibModule', path: 'b', loadChildren: '@nrwl/lib2/deep#LibModule' };` }; initialDeps.dependencies = { app1Name: [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], lib1Name: [], lib2Name: [ { projectName: 'lib3Name', type: DependencyType.es6Import } ] }; initialDeps.files = { 'app1.ts': [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], 'lib1.ts': [], 'lib2.ts': [ { projectName: 'lib1Name', type: DependencyType.es6Import } ] }; depsCalculator = new DepsCalculator( 'nrwl', projects, initialDeps, fileRead ); }); it('should be able to add edges to the graph', () => { virtualFs['lib1.ts'] = `import '@nrwl/lib2';`; depsCalculator.processFile('lib1.ts'); const deps = depsCalculator.getDeps(); expect(deps.lib1Name).toEqual([ { projectName: 'lib2Name', type: DependencyType.es6Import } ]); }); it('should be able to remove edges from the graph', () => { virtualFs['lib2.ts'] = ''; depsCalculator.processFile('lib2.ts'); const deps = depsCalculator.getDeps(); expect(deps.lib2Name).toEqual([]); }); it('should be able change the type of edges for the graph ', () => { virtualFs['app1.ts'] = ` import { LibModule } from '@nrwl/lib1'; import { Lib2Module } from '@nrwl/lib2';`; depsCalculator.processFile('app1.ts'); const deps = depsCalculator.getDeps(); expect(deps.app1Name).toEqual([ { projectName: 'lib1Name', type: DependencyType.es6Import }, { projectName: 'lib2Name', type: DependencyType.es6Import } ]); }); it('should be able to recalculate the same edges for the graph ', () => { virtualFs['app1.ts'] = ` const routes = { path: 'a', loadChildren: '@nrwl/lib1#LibModule', path: 'b', loadChildren: '@nrwl/lib2/deep#LibModule' };`; depsCalculator.processFile('app1.ts'); const deps = depsCalculator.getDeps(); expect(deps.app1Name).toEqual([ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ]); }); }); }); describe('Calculates Dependencies Between Apps and Libs', () => { describe('dependencies', () => { beforeEach(() => { spyOn(fs, 'writeFileSync'); }); it('should return a graph with a key for every project', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: [], fileMTimes: {}, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'app2Name', root: 'apps/app2', files: [], fileMTimes: {}, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app } ], null, () => null ); expect(deps).toEqual({ app1Name: [], app2Name: [] }); }); // NOTE: previously we did create an implicit dependency here, but that is now handled in `getProjectNodes` it('should not create implicit dependencies between e2e and apps', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: [], fileMTimes: {}, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'app1Name-e2e', root: 'apps/app1Name-e2e', files: [], fileMTimes: {}, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.e2e } ], null, () => null ); expect(deps).toEqual({ app1Name: [], 'app1Name-e2e': [] }); }); it('should support providing implicit deps for e2e project with custom name', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: [], fileMTimes: {}, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'customName-e2e', root: 'apps/customName-e2e', files: [], fileMTimes: {}, tags: [], implicitDependencies: ['app1Name'], architect: {}, type: ProjectType.e2e } ], null, () => null ); expect(deps).toEqual({ app1Name: [], 'customName-e2e': [ { projectName: 'app1Name', type: DependencyType.implicit } ] }); }); it('should support providing implicit deps for e2e project with standard name', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: [], fileMTimes: {}, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'app2Name', root: 'apps/app2', files: [], fileMTimes: {}, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'app1Name-e2e', root: 'apps/app1Name-e2e', files: [], fileMTimes: {}, tags: [], implicitDependencies: ['app2Name'], architect: {}, type: ProjectType.e2e } ], null, () => null ); expect(deps).toEqual({ app1Name: [], app2Name: [], 'app1Name-e2e': [ { projectName: 'app2Name', type: DependencyType.implicit } ] }); }); it('should infer deps between projects based on imports', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: ['app1.ts'], fileMTimes: { 'app1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.ts'], fileMTimes: { 'lib1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.ts'], fileMTimes: { 'lib2.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ], null, file => { switch (file) { case 'app1.ts': return ` import '@nrwl/lib1'; import '@nrwl/lib2/deep'; `; case 'lib1.ts': return `import '@nrwl/lib2'`; case 'lib2.ts': return ''; } } ); expect(deps).toEqual({ app1Name: [ { projectName: 'lib1Name', type: DependencyType.es6Import }, { projectName: 'lib2Name', type: DependencyType.es6Import } ], lib1Name: [{ projectName: 'lib2Name', type: DependencyType.es6Import }], lib2Name: [] }); }); it('should infer deps between projects based on exports', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: ['app1.ts'], fileMTimes: { 'app1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.ts'], fileMTimes: { 'lib1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.ts'], fileMTimes: { 'lib2.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ], null, file => { switch (file) { case 'app1.ts': return ` export * from '@nrwl/lib1'; export { } from '@nrwl/lib2/deep'; `; case 'lib1.ts': return `import '@nrwl/lib2'`; case 'lib2.ts': return ''; } } ); expect(deps).toEqual({ app1Name: [ { projectName: 'lib1Name', type: DependencyType.es6Import }, { projectName: 'lib2Name', type: DependencyType.es6Import } ], lib1Name: [{ projectName: 'lib2Name', type: DependencyType.es6Import }], lib2Name: [] }); }); it('should calculate dependencies in .tsx files', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: ['app1.tsx'], fileMTimes: { 'app1.tsx': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.tsx'], fileMTimes: { 'lib1.tsx': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.tsx'], fileMTimes: { 'lib2.tsx': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ], null, file => { switch (file) { case 'app1.tsx': return ` import '@nrwl/lib1'; import '@nrwl/lib2/deep'; `; case 'lib1.tsx': return `import '@nrwl/lib2'`; case 'lib2.tsx': return ''; } } ); expect(deps).toEqual({ app1Name: [ { projectName: 'lib1Name', type: DependencyType.es6Import }, { projectName: 'lib2Name', type: DependencyType.es6Import } ], lib1Name: [{ projectName: 'lib2Name', type: DependencyType.es6Import }], lib2Name: [] }); }); it('should calculate dependencies in .js files', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: ['app1.js'], fileMTimes: { 'app1.js': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.js'], fileMTimes: { 'lib1.js': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.js'], fileMTimes: { 'lib2.js': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ], null, file => { switch (file) { case 'app1.js': return ` import '@nrwl/lib1'; import '@nrwl/lib2/deep'; `; case 'lib1.js': return `import '@nrwl/lib2'`; case 'lib2.js': return ''; } } ); expect(deps).toEqual({ app1Name: [ { projectName: 'lib1Name', type: DependencyType.es6Import }, { projectName: 'lib2Name', type: DependencyType.es6Import } ], lib1Name: [{ projectName: 'lib2Name', type: DependencyType.es6Import }], lib2Name: [] }); }); it('should calculate dependencies in .jsx files', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: ['app1.jsx'], fileMTimes: { 'app1.jsx': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.jsx'], fileMTimes: { 'lib1.jsx': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.jsx'], fileMTimes: { 'lib2.jsx': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ], null, file => { switch (file) { case 'app1.jsx': return ` import '@nrwl/lib1'; import '@nrwl/lib2/deep'; `; case 'lib1.jsx': return `import '@nrwl/lib2'`; case 'lib2.jsx': return ''; } } ); expect(deps).toEqual({ app1Name: [ { projectName: 'lib1Name', type: DependencyType.es6Import }, { projectName: 'lib2Name', type: DependencyType.es6Import } ], lib1Name: [{ projectName: 'lib2Name', type: DependencyType.es6Import }], lib2Name: [] }); }); it('should infer dependencies expressed via loadChildren', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: ['app1.ts'], fileMTimes: { 'app1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.ts'], fileMTimes: { 'lib1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.ts'], fileMTimes: { 'lib2.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ], null, file => { switch (file) { case 'app1.ts': return ` const routes = { path: 'a', loadChildren: '@nrwl/lib1#LibModule', path: 'b', loadChildren: '@nrwl/lib2/deep#LibModule' }; `; case 'lib1.ts': return ''; case 'lib2.ts': return ''; } } ); expect(deps).toEqual({ app1Name: [ { projectName: 'lib1Name', type: DependencyType.loadChildren }, { projectName: 'lib2Name', type: DependencyType.loadChildren } ], lib1Name: [], lib2Name: [] }); }); it('should handle non-ts files', () => { const deps = dependencies( 'nrwl', [ { name: 'app1Name', root: 'apps/app1', files: ['index.html'], fileMTimes: { 'index.html': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app } ], null, () => null ); expect(deps).toEqual({ app1Name: [] }); }); it('should handle projects with the names starting with the same string', () => { const deps = dependencies( 'nrwl', [ { name: 'aaName', root: 'libs/aa', files: ['aa.ts'], fileMTimes: { 'aa.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'aaBbName', root: 'libs/aa/bb', files: ['bb.ts'], fileMTimes: { 'bb.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app } ], null, file => { switch (file) { case 'aa.ts': return `import '@nrwl/aa/bb'`; case 'bb.ts': return ''; } } ); expect(deps).toEqual({ aaBbName: [], aaName: [{ projectName: 'aaBbName', type: DependencyType.es6Import }] }); }); it('should not add the same dependency twice', () => { const deps = dependencies( 'nrwl', [ { name: 'aaName', root: 'libs/aa', files: ['aa.ts'], fileMTimes: { 'aa.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app }, { name: 'bbName', root: 'libs/bb', files: ['bb.ts'], fileMTimes: { 'bb.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app } ], null, file => { switch (file) { case 'aa.ts': return ` import '@nrwl/bb/bb' import '@nrwl/bb/bb' `; case 'bb.ts': return ''; } } ); expect(deps).toEqual({ aaName: [{ projectName: 'bbName', type: DependencyType.es6Import }], bbName: [] }); }); it('should not add a dependency on self', () => { const deps = dependencies( 'nrwl', [ { name: 'aaName', root: 'libs/aa', files: ['aa.ts'], fileMTimes: { 'aa.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.app } ], null, file => { switch (file) { case 'aa.ts': return ` import '@nrwl/aa/aa' `; } } ); expect(deps).toEqual({ aaName: [] }); }); it(`should handle an ExportDeclaration w/ moduleSpecifier and w/o moduleSpecifier`, () => { const deps = dependencies( 'nrwl', [ { name: 'lib1Name', root: 'libs/lib1', files: ['lib1.ts'], fileMTimes: { 'lib1.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib2Name', root: 'libs/lib2', files: ['lib2.ts'], fileMTimes: { 'lib2.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib }, { name: 'lib3Name', root: 'libs/lib3', files: ['lib3.ts'], fileMTimes: { 'lib3.ts': 1 }, tags: [], implicitDependencies: [], architect: {}, type: ProjectType.lib } ], null, file => { switch (file) { case 'lib1.ts': return ` const FOO = 23; export { FOO }; `; case 'lib2.ts': return ` export const BAR = 24; `; case 'lib3.ts': return ` import { FOO } from '@nrwl/lib1'; export { FOO }; export { BAR } from '@nrwl/lib2'; `; } } ); expect(deps).toEqual({ lib1Name: [], lib2Name: [], lib3Name: [ { projectName: 'lib1Name', type: DependencyType.es6Import }, { projectName: 'lib2Name', type: DependencyType.es6Import } ] }); }); }); });