diff --git a/e2e/schematics/command-line.test.ts b/e2e/schematics/command-line.test.ts index cc2626ac8e84c..3163fcea075d4 100644 --- a/e2e/schematics/command-line.test.ts +++ b/e2e/schematics/command-line.test.ts @@ -114,6 +114,7 @@ describe('Command line', () => { () => { newProject(); newApp('myapp'); + newLib('mylib'); updateFile( 'apps/myapp/src/main.ts', ` @@ -135,14 +136,26 @@ describe('Command line', () => { ` ); + updateFile( + 'libs/mylib/index.ts', + ` + const x = 1111; + ` + ); + updateFile( + 'libs/mylib/src/mylib.module.ts', + ` + const y = 1111; + ` + ); + try { - // this will group it by app, so all three files will be "marked" - runCommand('npm run -s format:check -- --files="apps/myapp/src/app/app.module.ts" --libs-and-apps'); + // this will group it by lib, so all three files will be "marked" + runCommand('npm run -s format:check -- --files="libs/mylib/index.ts" --libs-and-apps'); fail('boom'); } catch (e) { - expect(e.stdout.toString()).toContain('apps/myapp/src/main.ts'); - expect(e.stdout.toString()).toContain('apps/myapp/src/app/app.module.ts'); - expect(e.stdout.toString()).toContain('apps/myapp/src/app/app.component.ts'); + expect(e.stdout.toString()).toContain('libs/mylib/index.ts'); + expect(e.stdout.toString()).toContain('libs/mylib/src/mylib.module.ts'); } try { diff --git a/packages/schematics/src/command-line/affected-apps.spec.ts b/packages/schematics/src/command-line/affected-apps.spec.ts index a7aace6980730..59078976eddd5 100644 --- a/packages/schematics/src/command-line/affected-apps.spec.ts +++ b/packages/schematics/src/command-line/affected-apps.spec.ts @@ -1,4 +1,4 @@ -import { affectedApps, dependencies } from './affected-apps'; +import {affectedApps, dependencies, DependencyType, ProjectType, touchedProjects} from './affected-apps'; describe('Calculates Dependencies Between Apps and Libs', () => { describe('dependencies', () => { @@ -8,19 +8,21 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: [], - isApp: true + type: ProjectType.app }, { name: 'app2', + root: '', files: [], - isApp: true + type: ProjectType.app } ], () => null ); - expect(deps).toEqual({ app1: ['app1'], app2: ['app2'] }); + expect(deps).toEqual({ app1: [], app2: [] }); }); it('should infer deps between projects based on imports', () => { @@ -29,18 +31,21 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: ['app1.ts'], - isApp: true + type: ProjectType.app }, { name: 'lib1', + root: '', files: ['lib1.ts'], - isApp: false + type: ProjectType.lib }, { name: 'lib2', + root: '', files: ['lib2.ts'], - isApp: false + type: ProjectType.lib } ], file => { @@ -58,44 +63,13 @@ describe('Calculates Dependencies Between Apps and Libs', () => { } ); - expect(deps).toEqual({ app1: ['app1', 'lib1', 'lib2'], lib1: ['lib1', 'lib2'], lib2: ['lib2'] }); - }); - - it('should infer transitive deps between projects', () => { - const deps = dependencies( - 'nrwl', - [ - { - name: 'app1', - files: ['app1.ts'], - isApp: true - }, - { - name: 'lib1', - files: ['lib1.ts'], - isApp: false - }, - { - name: 'lib2', - files: ['lib2.ts'], - isApp: false - } - ], - file => { - switch (file) { - case 'app1.ts': - return ` - import '@nrwl/lib1'; - `; - case 'lib1.ts': - return `import '@nrwl/lib2'`; - case 'lib2.ts': - return ''; - } - } - ); - - expect(deps).toEqual({ app1: ['app1', 'lib1', 'lib2'], lib1: ['lib1', 'lib2'], lib2: ['lib2'] }); + expect(deps).toEqual({ + app1: [ + {projectName: 'lib1', type: DependencyType.es6Import}, + {projectName: 'lib2', type: DependencyType.es6Import} + ], + lib1: [{projectName: 'lib2', type: DependencyType.es6Import}], lib2: [] + }); }); it('should infer dependencies expressed via loadChildren', () => { @@ -104,18 +78,21 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: ['app1.ts'], - isApp: true + type: ProjectType.app }, { name: 'lib1', + root: '', files: ['lib1.ts'], - isApp: false + type: ProjectType.lib }, { name: 'lib2', + root: '', files: ['lib2.ts'], - isApp: false + type: ProjectType.lib } ], file => { @@ -135,7 +112,8 @@ describe('Calculates Dependencies Between Apps and Libs', () => { } ); - expect(deps).toEqual({ app1: ['app1', 'lib1', 'lib2'], lib1: ['lib1'], lib2: ['lib2'] }); + expect(deps).toEqual({ app1: [{projectName: 'lib1', type: DependencyType.loadChildren}, + {projectName: 'lib2', type: DependencyType.loadChildren}], lib1: [], lib2: [] }); }); it('should handle non-ts files', () => { @@ -144,14 +122,15 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: ['index.html'], - isApp: true + type: ProjectType.app } ], () => null ); - expect(deps).toEqual({ app1: ['app1'] }); + expect(deps).toEqual({ app1: [] }); }); it('should handle projects with the names starting with the same string', () => { @@ -160,13 +139,15 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'aa', + root: '', files: ['aa.ts'], - isApp: true + type: ProjectType.app }, { name: 'aa/bb', + root: '', files: ['bb.ts'], - isApp: true + type: ProjectType.app } ], file => { @@ -179,7 +160,7 @@ describe('Calculates Dependencies Between Apps and Libs', () => { } ); - expect(deps).toEqual({ aa: ['aa', 'aa/bb'], 'aa/bb': ['aa/bb'] }); + expect(deps).toEqual({ aa: [{projectName: 'aa/bb', type: DependencyType.es6Import}], 'aa/bb': [] }); }); }); @@ -190,23 +171,27 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: ['app1.ts'], - isApp: true + type: ProjectType.app }, { name: 'app2', + root: '', files: ['app2.ts'], - isApp: true + type: ProjectType.app }, { name: 'lib1', + root: '', files: ['lib1.ts'], - isApp: false + type: ProjectType.lib }, { name: 'lib2', + root: '', files: ['lib2.ts'], - isApp: false + type: ProjectType.lib } ], file => { @@ -235,18 +220,21 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: ['app1.ts'], - isApp: true + type: ProjectType.app }, { name: 'app2', + root: '', files: ['app2.ts'], - isApp: true + type: ProjectType.app }, { name: 'lib1', + root: '', files: ['lib1.ts'], - isApp: false + type: ProjectType.lib } ], file => { @@ -273,8 +261,9 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: ['one\\app1.ts', 'two/app1.ts'], - isApp: true + type: ProjectType.app } ], file => { @@ -297,13 +286,15 @@ describe('Calculates Dependencies Between Apps and Libs', () => { [ { name: 'app1', + root: '', files: ['app1.ts'], - isApp: true + type: ProjectType.app }, { name: 'app2', + root: '', files: ['app2.ts'], - isApp: true + type: ProjectType.app } ], file => { @@ -320,4 +311,40 @@ describe('Calculates Dependencies Between Apps and Libs', () => { expect(affected).toEqual(['app2', 'app1']); }); }); + + describe('touchedProjects', () => { + it('should return the list of touchedProjects', () => { + const tp = touchedProjects( + [ + { + name: 'app1', + root: '', + files: ['app1.ts'], + type: ProjectType.app + }, + { + name: 'app2', + root: '', + files: ['app2.ts'], + type: ProjectType.app + }, + { + name: 'lib1', + root: '', + files: ['lib1.ts'], + type: ProjectType.lib + }, + { + name: 'lib2', + root: '', + files: ['lib2.ts'], + type: ProjectType.lib + } + ], + ['lib2.ts', 'app2.ts', 'package.json'] + ); + + expect(tp).toEqual(['lib2', 'app2', null]); + }); + }); }); diff --git a/packages/schematics/src/command-line/affected-apps.ts b/packages/schematics/src/command-line/affected-apps.ts index a40caad573d65..febe5ffe540ad 100644 --- a/packages/schematics/src/command-line/affected-apps.ts +++ b/packages/schematics/src/command-line/affected-apps.ts @@ -1,33 +1,51 @@ import * as ts from 'typescript'; import * as path from 'path'; -export type Project = { name: string; isApp: boolean; files: string[] }; +export enum ProjectType { + app = 'app', + lib = 'lib' +} + +export enum DependencyType { + es6Import = 'es6Import', + loadChildren = 'loadChildren' +} + +export type ProjectNode = { name: string; root: string; type: ProjectType; files: string[] }; +export type Dependency = { projectName: string; type: DependencyType }; + +export function touchedProjects(projects: ProjectNode[], touchedFiles: string[]) { + projects = normalizeProjects(projects); + touchedFiles = normalizeFiles(touchedFiles); + return touchedFiles.map(f => { + const p = projects.filter(project => project.files.indexOf(f) > -1)[0]; + return p ? p.name : null; + }); +} export function affectedApps( npmScope: string, - projects: Project[], + projects: ProjectNode[], fileRead: (s: string) => string, touchedFiles: string[] ): string[] { projects = normalizeProjects(projects); - touchedFiles = normalizeFiles(touchedFiles); const deps = dependencies(npmScope, projects, fileRead); - - const touchedProjects = touchedFiles.map(f => { - const p = projects.filter(project => project.files.indexOf(f) > -1)[0]; - return p ? p.name : null; - }); - - if (touchedProjects.indexOf(null) > -1) { - return projects.filter(p => p.isApp).map(p => p.name); + const tp = touchedProjects(projects, touchedFiles); + if (tp.indexOf(null) > -1) { + return projects.filter(p => p.type === ProjectType.app).map(p => p.name); + } else { + return projects.filter(p => p.type === ProjectType.app).map(p => p.name).filter(name => hasDependencyOnTouchedProjects(name, tp, deps, [])); } +} - return projects - .filter(p => p.isApp && deps[p.name].filter(dep => touchedProjects.indexOf(dep) > -1).length > 0) - .map(p => p.name); +function hasDependencyOnTouchedProjects(project: string, touchedProjects: string[], deps: { [projectName: string]: Dependency[] }, visisted: string[]) { + if (touchedProjects.indexOf(project) > -1) return true; + if (visisted.indexOf(project) > -1) return false; + return deps[project].map(d => d.projectName).filter(k => hasDependencyOnTouchedProjects(k, touchedProjects, deps, [...visisted, project])).length > 0; } -function normalizeProjects(projects: Project[]) { +function normalizeProjects(projects: ProjectNode[]) { return projects.map(p => { return { ...p, @@ -42,16 +60,16 @@ function normalizeFiles(files: string[]): string[] { export function dependencies( npmScope: string, - projects: Project[], + projects: ProjectNode[], fileRead: (s: string) => string -): { [appName: string]: string[] } { +): { [projectName: string]: Dependency[] } { return new Deps(npmScope, projects, fileRead).calculateDeps(); } class Deps { - private deps: { [appName: string]: string[] }; + private deps: { [projectName: string]: Dependency[] }; - constructor(private npmScope: string, private projects: Project[], private fileRead: (s: string) => string) { + constructor(private npmScope: string, private projects: ProjectNode[], private fileRead: (s: string) => string) { this.projects.sort((a, b) => { if (!a.name) return -1; if (!b.name) return -1; @@ -60,9 +78,10 @@ class Deps { } calculateDeps() { - this.deps = this.projects.reduce((m, c) => ({ ...m, [c.name]: [] }), {}); + this.deps = this.projects.reduce((m, c) => ({...m, [c.name]: []}), {}); this.processAllFiles(); - return this.includeTransitive(); + return this.deps; + // return this.includeTransitive(); } private processAllFiles() { @@ -73,26 +92,6 @@ class Deps { }); } - private includeTransitive() { - const res = {}; - Object.keys(this.deps).forEach(project => { - res[project] = this.transitiveDeps(project, [project]); - }); - return res; - } - - private transitiveDeps(project: string, path: string[]): string[] { - let res = [project]; - - this.deps[project].forEach(d => { - if (path.indexOf(d) === -1) { - res = [...res, ...this.transitiveDeps(d, [...path, d])]; - } - }); - - return Array.from(new Set(res)); - } - private processFile(projectName: string, filePath: string): void { if (path.extname(filePath) === '.ts') { const tsFile = ts.createSourceFile(filePath, this.fileRead(filePath), ts.ScriptTarget.Latest, true); @@ -103,7 +102,7 @@ class Deps { private processNode(projectName: string, node: ts.Node): void { if (node.kind === ts.SyntaxKind.ImportDeclaration) { const imp = this.getStringLiteralValue((node as ts.ImportDeclaration).moduleSpecifier); - this.addDepIfNeeded(imp, projectName); + this.addDepIfNeeded(imp, projectName, DependencyType.es6Import); return; // stop traversing downwards } @@ -113,7 +112,7 @@ class Deps { const init = (node as ts.PropertyAssignment).initializer; if (init.kind === ts.SyntaxKind.StringLiteral) { const childrenExpr = this.getStringLiteralValue(init); - this.addDepIfNeeded(childrenExpr, projectName); + this.addDepIfNeeded(childrenExpr, projectName, DependencyType.loadChildren); return; // stop traversing downwards } } @@ -135,7 +134,7 @@ class Deps { } } - private addDepIfNeeded(expr: string, projectName: string) { + private addDepIfNeeded(expr: string, projectName: string, depType: DependencyType) { const matchingProject = this.projectNames.filter( a => expr === `@${this.npmScope}/${a}` || @@ -144,7 +143,7 @@ class Deps { )[0]; if (matchingProject) { - this.deps[projectName].push(matchingProject); + this.deps[projectName].push({projectName: matchingProject, type: depType}); } } diff --git a/packages/schematics/src/command-line/format.ts b/packages/schematics/src/command-line/format.ts index 8bd0ba3452677..924e99f67cba8 100644 --- a/packages/schematics/src/command-line/format.ts +++ b/packages/schematics/src/command-line/format.ts @@ -1,7 +1,7 @@ import { execSync } from 'child_process'; import * as path from 'path'; import * as resolve from 'resolve'; -import { getAffectedApps, getAppRoots, parseFiles } from './shared'; +import {getProjectRoots, getTouchedProjects, parseFiles} from './shared'; export function format(args: string[]) { const command = args[0]; @@ -38,7 +38,7 @@ function getPatterns(args: string[]) { } function getPatternsFromApps(affectedFiles: string[]): string[] { - const roots = getAppRoots(getAffectedApps(affectedFiles)); + const roots = getProjectRoots(getTouchedProjects(affectedFiles)); if (roots.length === 0) { return []; } else if (roots.length === 1) { diff --git a/packages/schematics/src/command-line/shared.ts b/packages/schematics/src/command-line/shared.ts index 0b3a4d92da08f..48d50ab2862ce 100644 --- a/packages/schematics/src/command-line/shared.ts +++ b/packages/schematics/src/command-line/shared.ts @@ -1,6 +1,6 @@ import { execSync } from 'child_process'; import * as path from 'path'; -import { affectedApps } from './affected-apps'; +import {affectedApps, ProjectType, touchedProjects} from './affected-apps'; import * as fs from 'fs'; export function parseFiles(args: string[]): { files: string[]; rest: string[] } { @@ -41,15 +41,20 @@ function getFilesFromShash(sha1: string, sha2: string): string[] { .filter(a => a.length > 0); } -export function getAffectedApps(touchedFiles: string[]): string[] { - const config = JSON.parse(fs.readFileSync('.angular-cli.json', 'utf-8')); - const projects = (config.apps ? config.apps : []).filter(p => p.name !== '$workspaceRoot').map(p => { +function getProjectNodes(config) { + return (config.apps ? config.apps : []).filter(p => p.name !== '$workspaceRoot').map(p => { return { name: p.name, - isApp: p.root.startsWith('apps/'), + root: p.root, + type: p.root.startsWith('apps/') ? ProjectType.app : ProjectType.lib, files: allFilesInDir(path.dirname(p.root)) }; }); +} + +export function getAffectedApps(touchedFiles: string[]): string[] { + const config = JSON.parse(fs.readFileSync('.angular-cli.json', 'utf-8')); + const projects = getProjectNodes(config); if (!config.project.npmScope) { throw new Error(`.angular-cli.json must define the npmScope property.`); @@ -58,9 +63,19 @@ export function getAffectedApps(touchedFiles: string[]): string[] { return affectedApps(config.project.npmScope, projects, f => fs.readFileSync(f, 'utf-8'), touchedFiles); } -export function getAppRoots(appNames: string[]): string[] { +export function getTouchedProjects(touchedFiles: string[]): string[] { + const config = JSON.parse(fs.readFileSync('.angular-cli.json', 'utf-8')); + const projects = getProjectNodes(config); + if (!config.project.npmScope) { + throw new Error(`.angular-cli.json must define the npmScope property.`); + } + return touchedProjects(projects, touchedFiles).filter(p => !!p); +} + +export function getProjectRoots(projectNames: string[]): string[] { const config = JSON.parse(fs.readFileSync('.angular-cli.json', 'utf-8')); - return (config.apps ? config.apps : []).filter(p => p.name !== '$workspaceRoot').filter(p => appNames.indexOf(p.name) > -1).map(p => path.dirname(p.root)); + const projects = getProjectNodes(config); + return projectNames.map(name => path.dirname(projects.filter(p => p.name === name)[0].root)); } function allFilesInDir(dirName: string): string[] {