feat(nx): support to generate visualization graph
`npm run dep-graph` outputs a visual dependency graph
This commit is contained in:
parent
eca52599d9
commit
49525efe3e
@ -286,4 +286,136 @@ describe('Command line', () => {
|
||||
},
|
||||
1000000
|
||||
);
|
||||
describe('dep-graph', () => {
|
||||
beforeEach(() => {
|
||||
newProject();
|
||||
newApp('myapp');
|
||||
newApp('myapp2');
|
||||
newApp('myapp3');
|
||||
newLib('mylib');
|
||||
newLib('mylib2');
|
||||
|
||||
updateFile(
|
||||
'apps/myapp/src/main.ts',
|
||||
`
|
||||
import '@proj/mylib';
|
||||
|
||||
const s = {loadChildren: '@proj/mylib2'};
|
||||
`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
'apps/myapp2/src/app/app.component.spec.ts',
|
||||
`import '@proj/mylib';`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
'libs/mylib/src/mylib.module.spec.ts',
|
||||
`import '@proj/mylib2';`
|
||||
);
|
||||
});
|
||||
|
||||
it(
|
||||
'dep-graph should output json (without critical path) to file',
|
||||
() => {
|
||||
const file = 'dep-graph.json';
|
||||
|
||||
runCommand(`npm run dep-graph -- --file="${file}"`);
|
||||
|
||||
expect(() => checkFilesExist(file)).not.toThrow();
|
||||
|
||||
const jsonFileContents = readJson(file);
|
||||
|
||||
expect(jsonFileContents).toEqual({
|
||||
deps: {
|
||||
mylib2: [],
|
||||
myapp3: [],
|
||||
myapp2: [
|
||||
{
|
||||
projectName: 'mylib',
|
||||
type: 'es6Import'
|
||||
}
|
||||
],
|
||||
mylib: [
|
||||
{
|
||||
projectName: 'mylib2',
|
||||
type: 'es6Import'
|
||||
}
|
||||
],
|
||||
myapp: [
|
||||
{
|
||||
projectName: 'mylib',
|
||||
type: 'es6Import'
|
||||
},
|
||||
{
|
||||
projectName: 'mylib2',
|
||||
type: 'loadChildren'
|
||||
}
|
||||
]
|
||||
},
|
||||
criticalPath: []
|
||||
});
|
||||
},
|
||||
1000000
|
||||
);
|
||||
|
||||
it(
|
||||
'dep-graph should output json with critical path to file',
|
||||
() => {
|
||||
const file = 'dep-graph.json';
|
||||
|
||||
runCommand(
|
||||
`npm run affected:dep-graph -- --files="libs/mylib/index.ts" --file="${file}"`
|
||||
);
|
||||
|
||||
expect(() => checkFilesExist(file)).not.toThrow();
|
||||
|
||||
const jsonFileContents = readJson(file);
|
||||
|
||||
expect(jsonFileContents.criticalPath).toContain('myapp');
|
||||
expect(jsonFileContents.criticalPath).toContain('myapp2');
|
||||
expect(jsonFileContents.criticalPath).toContain('mylib');
|
||||
expect(jsonFileContents.criticalPath).not.toContain('mylib2');
|
||||
},
|
||||
1000000
|
||||
);
|
||||
|
||||
it(
|
||||
'dep-graph should output dot to file',
|
||||
() => {
|
||||
const file = 'dep-graph.dot';
|
||||
|
||||
runCommand(
|
||||
`npm run dep-graph -- --files="libs/mylib/index.ts" --file="${file}"`
|
||||
);
|
||||
|
||||
expect(() => checkFilesExist(file)).not.toThrow();
|
||||
|
||||
const fileContents = readFile(file);
|
||||
expect(fileContents).toContain('"myapp" -> "mylib"');
|
||||
expect(fileContents).toContain('"myapp2" -> "mylib"');
|
||||
expect(fileContents).toContain('"mylib" -> "mylib2"');
|
||||
},
|
||||
1000000
|
||||
);
|
||||
|
||||
it(
|
||||
'dep-graph should output html to file',
|
||||
() => {
|
||||
const file = 'dep-graph.html';
|
||||
runCommand(
|
||||
`npm run dep-graph -- --files="libs/mylib/index.ts" --file="${file}"`
|
||||
);
|
||||
|
||||
expect(() => checkFilesExist(file)).not.toThrow();
|
||||
|
||||
const fileContents = readFile(file);
|
||||
expect(fileContents).toContain('<html>');
|
||||
expect(fileContents).toContain('<title>myapp->mylib</title>');
|
||||
expect(fileContents).toContain('<title>myapp->mylib2</title>');
|
||||
expect(fileContents).toContain('<title>mylib->mylib2</title>');
|
||||
},
|
||||
1000000
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
"copy": "./scripts/copy.sh",
|
||||
"test:schematics": "yarn linknpm fast && ./scripts/test_schematics.sh",
|
||||
"test:nx": "yarn linknpm fast && ./scripts/test_nx.sh",
|
||||
"test":
|
||||
"yarn linknpm fast && ./scripts/test_nx.sh && ./scripts/test_schematics.sh",
|
||||
"test": "yarn linknpm fast && ./scripts/test_nx.sh && ./scripts/test_schematics.sh",
|
||||
"checkformat": "prettier \"{packages,e2e}/**/*.ts\" --list-different",
|
||||
"publish_npm": "./scripts/publish.sh"
|
||||
},
|
||||
@ -44,6 +43,7 @@
|
||||
"angular": "1.6.6",
|
||||
"app-root-path": "^2.0.1",
|
||||
"cosmiconfig": "^4.0.0",
|
||||
"graphviz": "^0.0.8",
|
||||
"husky": "^0.14.3",
|
||||
"jasmine-core": "~2.8.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
@ -52,8 +52,9 @@
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-webpack": "2.0.4",
|
||||
"npm-run-all": "4.1.2",
|
||||
"ng-packagr": "2.2.0",
|
||||
"npm-run-all": "4.1.2",
|
||||
"opn": "^5.3.0",
|
||||
"precise-commits": "1.0.0",
|
||||
"prettier": "1.10.2",
|
||||
"rxjs": "^5.5.6",
|
||||
@ -62,6 +63,7 @@
|
||||
"tmp": "0.0.33",
|
||||
"tslint": "5.9.1",
|
||||
"typescript": "2.6.2",
|
||||
"viz.js": "^1.8.1",
|
||||
"yargs-parser": "9.0.2",
|
||||
"zone.js": "^0.8.19",
|
||||
"fs-extra": "5.0.0"
|
||||
|
||||
@ -12,12 +12,14 @@
|
||||
"affected:apps": "./node_modules/.bin/nx affected apps",
|
||||
"affected:build": "./node_modules/.bin/nx affected build",
|
||||
"affected:e2e": "./node_modules/.bin/nx affected e2e",
|
||||
"affected:dep-graph": "./node_modules/.bin/nx affected dep-graph",
|
||||
"format": "./node_modules/.bin/nx format write",
|
||||
"format:write": "./node_modules/.bin/nx format write",
|
||||
"format:check": "./node_modules/.bin/nx format check",
|
||||
"update": "./node_modules/.bin/nx update",
|
||||
"update:check": "./node_modules/.bin/nx update check",
|
||||
"update:skip": "./node_modules/.bin/nx update skip",
|
||||
"dep-graph": "./node_modules/.bin/nx dep-graph",
|
||||
"postinstall": "ngc -p ngc.tsconfig.json"
|
||||
},
|
||||
"private": true,
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function writeToFile(path: string, str: string) {
|
||||
fs.writeFileSync(path, str);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is specifically for updating a JSON file using the filesystem
|
||||
*
|
||||
@ -12,7 +16,7 @@ import * as path from 'path';
|
||||
export function updateJsonFile(path: string, callback: (a: any) => any) {
|
||||
const json = readJsonFile(path);
|
||||
callback(json);
|
||||
fs.writeFileSync(path, JSON.stringify(json, null, 2));
|
||||
writeToFile(path, JSON.stringify(json, null, 2));
|
||||
}
|
||||
|
||||
export function addApp(apps: any[] | undefined, newApp: any): any[] {
|
||||
@ -59,3 +63,17 @@ export function copyFile(file: string, target: string) {
|
||||
source.pipe(dest);
|
||||
source.on('error', e => console.error(e));
|
||||
}
|
||||
|
||||
function directoryExists(name) {
|
||||
try {
|
||||
return fs.statSync(name).isDirectory();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDirectory(name: string) {
|
||||
if (!directoryExists(name)) {
|
||||
fs.mkdirSync(name);
|
||||
}
|
||||
}
|
||||
|
||||
14
packages/schematics/migrations/20180404-adding-dep-graph.ts
Normal file
14
packages/schematics/migrations/20180404-adding-dep-graph.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { updateJsonFile } from '../src/utils/fileutils';
|
||||
|
||||
export default {
|
||||
description: 'Update npm scripts to use the nx command',
|
||||
run: () => {
|
||||
updateJsonFile('package.json', json => {
|
||||
json.scripts = {
|
||||
...json.scripts,
|
||||
'dep-graph': './node_modules/.bin/nx dep-graph',
|
||||
'affected:dep-graph': './node_modules/.bin/nx affected dep-graph'
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -20,7 +20,7 @@
|
||||
},
|
||||
"main": "index.js",
|
||||
"types": "index.d.js",
|
||||
"peerDependencies": { },
|
||||
"peerDependencies": {},
|
||||
"author": "Victor Savkin",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
@ -32,9 +32,12 @@
|
||||
"@ngrx/schematics": "5.2.0",
|
||||
"@schematics/angular": "0.4.6",
|
||||
"app-root-path": "^2.0.1",
|
||||
"graphviz": "0.0.8",
|
||||
"npm-run-all": "4.1.2",
|
||||
"opn": "^5.3.0",
|
||||
"semver": "5.4.1",
|
||||
"tmp": "0.0.33",
|
||||
"viz.js": "^1.8.1",
|
||||
"yargs-parser": "9.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -9,20 +9,18 @@
|
||||
"test": "ng test",
|
||||
"lint": "./node_modules/.bin/nx lint && ng lint",
|
||||
"e2e": "ng e2e",
|
||||
|
||||
"affected:apps": "./node_modules/.bin/nx affected apps",
|
||||
"affected:build": "./node_modules/.bin/nx affected build",
|
||||
"affected:e2e": "./node_modules/.bin/nx affected e2e",
|
||||
|
||||
"affected:dep-graph": "./node_modules/.bin/nx affected dep-graph",
|
||||
"format": "./node_modules/.bin/nx format write",
|
||||
"format:write": "./node_modules/.bin/nx format write",
|
||||
"format:check": "./node_modules/.bin/nx format check",
|
||||
|
||||
"update": "./node_modules/.bin/nx update",
|
||||
"update:check": "./node_modules/.bin/nx update check",
|
||||
"update:skip": "./node_modules/.bin/nx update skip",
|
||||
|
||||
"workspace-schematic": "./node_modules/.bin/nx workspace-schematic",
|
||||
"dep-graph": "./node_modules/.bin/nx dep-graph",
|
||||
"postinstall": "./node_modules/.bin/nx postinstall"
|
||||
},
|
||||
"private": true,
|
||||
@ -43,7 +41,8 @@
|
||||
"@ngrx/router-store": "<%= routerStoreVersion %>",
|
||||
"@ngrx/store": "<%= ngrxVersion %>",
|
||||
"@ngrx/store-devtools": "<%= ngrxVersion %>",
|
||||
"ngrx-store-freeze": "<%= ngrxStoreFreezeVersion %>"
|
||||
"ngrx-store-freeze": "<%= ngrxStoreFreezeVersion %>",
|
||||
"viz": "^0.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "<%= angularCliVersion %>",
|
||||
@ -52,19 +51,20 @@
|
||||
"@nrwl/schematics": "<%= schematicsVersion %>",
|
||||
"@angular/language-service": "<%= angularVersion %>",<% if (!minimal) { %>
|
||||
"@types/jasmine": "~2.8.3",
|
||||
"@types/jasminewd2": "~2.0.2",
|
||||
"@types/node": "~6.0.60",
|
||||
"codelyzer": "^4.0.1",
|
||||
"jasmine-core": "~2.8.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~2.0.0",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~4.1.0",
|
||||
"tslint": "~5.9.1",<% } %>
|
||||
"@types/jasminewd2": "~2.0.2",
|
||||
"@types/node": "~6.0.60",
|
||||
"codelyzer": "^4.0.1",
|
||||
"jasmine-core": "~2.8.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~2.0.0",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "^1.2.1",
|
||||
"karma-jasmine": "~1.1.0",
|
||||
"karma-jasmine-html-reporter": "^0.2.2",
|
||||
"protractor": "~5.1.2",
|
||||
"ts-node": "~4.1.0",
|
||||
"tslint": "~5.9.1",<%
|
||||
} %>
|
||||
"typescript": "<%= typescriptVersion %>",
|
||||
"prettier": "<%= prettierVersion %>"
|
||||
}
|
||||
|
||||
@ -85,6 +85,9 @@ function updatePackageJson() {
|
||||
'./node_modules/.bin/nx affected build';
|
||||
packageJson.scripts['affected:e2e'] = './node_modules/.bin/nx affected e2e';
|
||||
|
||||
packageJson.scripts['affected:dep-graph'] =
|
||||
'./node_modules/.bin/nx affected dep-graph';
|
||||
|
||||
packageJson.scripts['format'] = './node_modules/.bin/nx format write';
|
||||
packageJson.scripts['format:write'] = './node_modules/.bin/nx format write';
|
||||
packageJson.scripts['format:check'] = './node_modules/.bin/nx format check';
|
||||
@ -94,6 +97,9 @@ function updatePackageJson() {
|
||||
packageJson.scripts['update:skip'] = './node_modules/.bin/nx update skip';
|
||||
|
||||
packageJson.scripts['lint'] = './node_modules/.bin/nx lint && ng lint';
|
||||
|
||||
packageJson.scripts['dep-graph'] = './node_modules/.bin/nx dep-graph';
|
||||
|
||||
packageJson.scripts['postinstall'] = './node_modules/.bin/nx postinstall';
|
||||
packageJson.scripts['workspace-schematic'] =
|
||||
'./node_modules/.bin/nx workspace-schematic';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
affectedApps,
|
||||
affectedAppNames,
|
||||
dependencies,
|
||||
DependencyType,
|
||||
ProjectType,
|
||||
@ -254,9 +254,9 @@ describe('Calculates Dependencies Between Apps and Libs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('affectedApps', () => {
|
||||
describe('affectedAppNames', () => {
|
||||
it('should return the list of affected files', () => {
|
||||
const affected = affectedApps(
|
||||
const affected = affectedAppNames(
|
||||
'nrwl',
|
||||
[
|
||||
{
|
||||
@ -309,7 +309,7 @@ describe('Calculates Dependencies Between Apps and Libs', () => {
|
||||
});
|
||||
|
||||
it('should return app app names if a touched file is not part of a project', () => {
|
||||
const affected = affectedApps(
|
||||
const affected = affectedAppNames(
|
||||
'nrwl',
|
||||
[
|
||||
{
|
||||
@ -353,7 +353,7 @@ describe('Calculates Dependencies Between Apps and Libs', () => {
|
||||
});
|
||||
|
||||
it('should handle slashes', () => {
|
||||
const affected = affectedApps(
|
||||
const affected = affectedAppNames(
|
||||
'nrwl',
|
||||
[
|
||||
{
|
||||
@ -379,7 +379,7 @@ describe('Calculates Dependencies Between Apps and Libs', () => {
|
||||
});
|
||||
|
||||
it('should handle circular dependencies', () => {
|
||||
const affected = affectedApps(
|
||||
const affected = affectedAppNames(
|
||||
'nrwl',
|
||||
[
|
||||
{
|
||||
|
||||
@ -8,7 +8,8 @@ export enum ProjectType {
|
||||
|
||||
export enum DependencyType {
|
||||
es6Import = 'es6Import',
|
||||
loadChildren = 'loadChildren'
|
||||
loadChildren = 'loadChildren',
|
||||
implicit = 'implicit'
|
||||
}
|
||||
|
||||
export type ProjectNode = {
|
||||
@ -32,23 +33,51 @@ export function touchedProjects(
|
||||
});
|
||||
}
|
||||
|
||||
export function affectedApps(
|
||||
function affectedProjects(
|
||||
npmScope: string,
|
||||
projects: ProjectNode[],
|
||||
fileRead: (s: string) => string,
|
||||
touchedFiles: string[]
|
||||
): ProjectNode[] {
|
||||
projects = normalizeProjects(projects);
|
||||
const deps = dependencies(npmScope, projects, fileRead);
|
||||
const tp = touchedProjects(projects, touchedFiles);
|
||||
if (tp.indexOf(null) > -1) {
|
||||
return projects;
|
||||
} else {
|
||||
return projects.filter(proj =>
|
||||
hasDependencyOnTouchedProjects(proj.name, tp, deps, [])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export type AffectedFetcher = (
|
||||
npmScope: string,
|
||||
projects: ProjectNode[],
|
||||
fileRead: (s: string) => string,
|
||||
touchedFiles: string[]
|
||||
) => string[];
|
||||
|
||||
export function affectedAppNames(
|
||||
npmScope: string,
|
||||
projects: ProjectNode[],
|
||||
fileRead: (s: string) => string,
|
||||
touchedFiles: string[]
|
||||
): string[] {
|
||||
projects = normalizeProjects(projects);
|
||||
const deps = dependencies(npmScope, projects, fileRead);
|
||||
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 affectedProjects(npmScope, projects, fileRead, touchedFiles)
|
||||
.filter(p => p.type === ProjectType.app)
|
||||
.map(p => p.name);
|
||||
}
|
||||
|
||||
export function affectedProjectNames(
|
||||
npmScope: string,
|
||||
projects: ProjectNode[],
|
||||
fileRead: (s: string) => string,
|
||||
touchedFiles: string[]
|
||||
): string[] {
|
||||
return affectedProjects(npmScope, projects, fileRead, touchedFiles).map(
|
||||
p => p.name
|
||||
);
|
||||
}
|
||||
|
||||
function hasDependencyOnTouchedProjects(
|
||||
@ -86,16 +115,18 @@ function normalizeFiles(files: string[]): string[] {
|
||||
return files.map(f => f.replace(/[\\\/]+/g, '/'));
|
||||
}
|
||||
|
||||
export type Deps = { [projectName: string]: Dependency[] };
|
||||
|
||||
export function dependencies(
|
||||
npmScope: string,
|
||||
projects: ProjectNode[],
|
||||
fileRead: (s: string) => string
|
||||
): { [projectName: string]: Dependency[] } {
|
||||
return new Deps(npmScope, projects, fileRead).calculateDeps();
|
||||
): Deps {
|
||||
return new DepsCalculator(npmScope, projects, fileRead).calculateDeps();
|
||||
}
|
||||
|
||||
class Deps {
|
||||
private deps: { [projectName: string]: Dependency[] };
|
||||
class DepsCalculator {
|
||||
private deps: Deps;
|
||||
|
||||
constructor(
|
||||
private npmScope: string,
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { getAffectedApps, parseFiles } from './shared';
|
||||
import { getAffectedApps, getAffectedProjects, parseFiles } from './shared';
|
||||
import * as path from 'path';
|
||||
import * as resolve from 'resolve';
|
||||
import * as runAll from 'npm-run-all';
|
||||
import * as yargsParser from 'yargs-parser';
|
||||
import { generateGraph } from './dep-graph';
|
||||
|
||||
export function affected(args: string[]): void {
|
||||
const command = args[0];
|
||||
let apps: string[];
|
||||
let projects: string[];
|
||||
let rest: string[];
|
||||
try {
|
||||
const p = parseFiles(args.slice(1));
|
||||
rest = p.rest;
|
||||
apps = getAffectedApps(p.files);
|
||||
projects = getAffectedProjects(p.files);
|
||||
} catch (e) {
|
||||
printError(command, e);
|
||||
process.exit(1);
|
||||
@ -28,6 +31,9 @@ export function affected(args: string[]): void {
|
||||
case 'e2e':
|
||||
e2e(apps, rest);
|
||||
break;
|
||||
case 'dep-graph':
|
||||
generateGraph(yargsParser(rest), projects);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +45,7 @@ function printError(command: string, e: any) {
|
||||
`Or pass the list of files, as follows: npm run affected:${command} -- --files="libs/mylib/index.ts,libs/mylib2/index.ts".`
|
||||
);
|
||||
console.error(
|
||||
`Or to get the list of files from local changes: npm run affected:${command} -- --uncommitted | --untracked".`
|
||||
`Or to get the list of files from local changes: npm run affected:${command} -- --uncommitted | --untracked.`
|
||||
);
|
||||
console.error(e.message);
|
||||
}
|
||||
|
||||
5
packages/schematics/src/command-line/dep-graph.spec.ts
Normal file
5
packages/schematics/src/command-line/dep-graph.spec.ts
Normal file
@ -0,0 +1,5 @@
|
||||
describe('dep-graph', () => {
|
||||
describe('getNodeProps', () => {
|
||||
it('should get highlighted props for critical path', () => {});
|
||||
});
|
||||
});
|
||||
346
packages/schematics/src/command-line/dep-graph.ts
Normal file
346
packages/schematics/src/command-line/dep-graph.ts
Normal file
@ -0,0 +1,346 @@
|
||||
import { writeToFile } from '../utils/fileutils';
|
||||
import * as graphviz from 'graphviz';
|
||||
import * as appRoot from 'app-root-path';
|
||||
import * as opn from 'opn';
|
||||
import { readFileSync } from 'fs';
|
||||
import {
|
||||
ProjectNode,
|
||||
ProjectType,
|
||||
dependencies,
|
||||
Deps,
|
||||
Dependency,
|
||||
DependencyType
|
||||
} from './affected-apps';
|
||||
|
||||
import { readCliConfig, getProjectNodes } from './shared';
|
||||
import * as path from 'path';
|
||||
import { tmpNameSync } from 'tmp';
|
||||
|
||||
const viz = require('viz.js'); // typings are incorrect in viz.js library - need to use `require`
|
||||
|
||||
export enum NodeEdgeVariant {
|
||||
default = 'default',
|
||||
highlighted = 'highlighted'
|
||||
}
|
||||
|
||||
export type GraphvizOptions = {
|
||||
fontname?: string;
|
||||
fontsize?: number;
|
||||
shape?: string;
|
||||
color?: string;
|
||||
style?: string;
|
||||
fillcolor?: string;
|
||||
};
|
||||
export type AttrValue = {
|
||||
attr: string;
|
||||
value: boolean | number | string;
|
||||
};
|
||||
|
||||
export type GraphvizOptionNodeEdge = {
|
||||
[key: string]: {
|
||||
[variant: string]: GraphvizOptions;
|
||||
};
|
||||
};
|
||||
|
||||
export type GraphvizConfig = {
|
||||
graph: AttrValue[];
|
||||
nodes: GraphvizOptionNodeEdge;
|
||||
edges: GraphvizOptionNodeEdge;
|
||||
};
|
||||
|
||||
export type ProjectMap = {
|
||||
[name: string]: ProjectNode;
|
||||
};
|
||||
|
||||
export type CriticalPathMap = {
|
||||
[name: string]: boolean;
|
||||
};
|
||||
|
||||
export enum OutputType {
|
||||
'json' = 'json',
|
||||
'html' = 'html',
|
||||
'dot' = 'dot'
|
||||
}
|
||||
|
||||
export type UserOptions = {
|
||||
file?: string;
|
||||
output?: string;
|
||||
files?: string;
|
||||
};
|
||||
|
||||
type ParsedUserOptions = {
|
||||
isFilePresent?: boolean;
|
||||
filename?: string;
|
||||
type?: string;
|
||||
output?: string;
|
||||
shouldOpen: boolean;
|
||||
};
|
||||
|
||||
type OutputOptions = {
|
||||
data: string;
|
||||
shouldOpen: boolean;
|
||||
shouldWriteToFile: boolean;
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
type JSONOutput = {
|
||||
deps: Deps;
|
||||
criticalPath: string[];
|
||||
};
|
||||
|
||||
const defaultConfig = {
|
||||
isFilePresent: false,
|
||||
filename: undefined,
|
||||
type: OutputType.html,
|
||||
shouldOpen: true
|
||||
};
|
||||
|
||||
export const graphvizConfig: GraphvizConfig = {
|
||||
graph: [
|
||||
{
|
||||
attr: 'overlap',
|
||||
value: false
|
||||
},
|
||||
{
|
||||
attr: 'pad',
|
||||
value: 0.111
|
||||
}
|
||||
],
|
||||
nodes: {
|
||||
[ProjectType.app]: {
|
||||
[NodeEdgeVariant.default]: {
|
||||
fontname: 'Arial',
|
||||
fontsize: 14,
|
||||
shape: 'box'
|
||||
},
|
||||
[NodeEdgeVariant.highlighted]: {
|
||||
fontname: 'Arial',
|
||||
fontsize: 14,
|
||||
shape: 'box',
|
||||
color: '#FF0033'
|
||||
}
|
||||
},
|
||||
[ProjectType.lib]: {
|
||||
[NodeEdgeVariant.default]: {
|
||||
fontname: 'Arial',
|
||||
fontsize: 14,
|
||||
style: 'filled',
|
||||
fillcolor: '#EFEFEF'
|
||||
},
|
||||
[NodeEdgeVariant.highlighted]: {
|
||||
fontname: 'Arial',
|
||||
fontsize: 14,
|
||||
style: 'filled',
|
||||
fillcolor: '#EFEFEF',
|
||||
color: '#FF0033'
|
||||
}
|
||||
}
|
||||
},
|
||||
edges: {
|
||||
[DependencyType.es6Import]: {
|
||||
[NodeEdgeVariant.default]: {
|
||||
color: '#757575'
|
||||
},
|
||||
[NodeEdgeVariant.highlighted]: {
|
||||
color: '#FF0033'
|
||||
}
|
||||
},
|
||||
[DependencyType.loadChildren]: {
|
||||
[NodeEdgeVariant.default]: {
|
||||
color: '#757575',
|
||||
style: 'dotted'
|
||||
},
|
||||
[NodeEdgeVariant.highlighted]: {
|
||||
color: '#FF0033',
|
||||
style: 'dotted'
|
||||
}
|
||||
},
|
||||
[DependencyType.implicit]: {
|
||||
[NodeEdgeVariant.default]: {
|
||||
color: '#000000',
|
||||
style: 'bold'
|
||||
},
|
||||
[NodeEdgeVariant.highlighted]: {
|
||||
color: '#FF0033',
|
||||
style: 'bold'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function mapProjectNodes(projects: ProjectNode[]) {
|
||||
return projects.reduce((m, proj) => ({ ...m, [proj.name]: proj }), {});
|
||||
}
|
||||
|
||||
function getVariant(map: CriticalPathMap, key: string) {
|
||||
return map[key] ? NodeEdgeVariant.highlighted : NodeEdgeVariant.default;
|
||||
}
|
||||
|
||||
function getNodeProps(
|
||||
config: GraphvizOptionNodeEdge,
|
||||
projectNode: ProjectNode,
|
||||
criticalPath: CriticalPathMap
|
||||
) {
|
||||
const nodeProps = config[projectNode.type];
|
||||
return nodeProps[getVariant(criticalPath, projectNode.name)];
|
||||
}
|
||||
|
||||
function getEdgeProps(
|
||||
config: GraphvizOptionNodeEdge,
|
||||
depType: DependencyType,
|
||||
child: string,
|
||||
criticalPath: CriticalPathMap
|
||||
) {
|
||||
const edgeProps = config[depType];
|
||||
return edgeProps[getVariant(criticalPath, child)];
|
||||
}
|
||||
|
||||
export function createGraphviz(
|
||||
config: GraphvizConfig,
|
||||
deps: Deps,
|
||||
projects: ProjectNode[],
|
||||
criticalPath: CriticalPathMap
|
||||
) {
|
||||
const projectMap: ProjectMap = mapProjectNodes(projects);
|
||||
const g = graphviz.digraph('G');
|
||||
|
||||
config.graph.forEach(({ attr, value }) => g.set(attr, value));
|
||||
|
||||
Object.keys(deps)
|
||||
.sort() // sorting helps with testing
|
||||
.forEach(key => {
|
||||
const projectNode = projectMap[key];
|
||||
const dependencies = deps[key];
|
||||
|
||||
g.addNode(key, getNodeProps(config.nodes, projectNode, criticalPath));
|
||||
|
||||
if (dependencies.length > 0) {
|
||||
dependencies.forEach((dep: Dependency, i: number) => {
|
||||
g.addNode(
|
||||
dep.projectName,
|
||||
getNodeProps(config.nodes, projectNode, criticalPath)
|
||||
); // child node
|
||||
|
||||
g.addEdge(
|
||||
key,
|
||||
dep.projectName,
|
||||
getEdgeProps(config.edges, dep.type, dep.projectName, criticalPath)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return g.to_dot();
|
||||
}
|
||||
|
||||
function handleOutput({
|
||||
data,
|
||||
shouldOpen,
|
||||
shouldWriteToFile,
|
||||
filename
|
||||
}: OutputOptions) {
|
||||
if (shouldOpen) {
|
||||
const tmpFilename = `${tmpNameSync()}.html`;
|
||||
writeToFile(tmpFilename, data);
|
||||
opn(tmpFilename, {
|
||||
wait: false
|
||||
});
|
||||
} else if (!shouldWriteToFile) {
|
||||
return console.log(data);
|
||||
} else {
|
||||
writeToFile(filename, data);
|
||||
}
|
||||
}
|
||||
|
||||
function applyHTMLTemplate(svg: string) {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title></title></head>
|
||||
<body>${svg}</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
function generateGraphJson(criticalPath?: string[]): JSONOutput {
|
||||
const config = readCliConfig();
|
||||
const npmScope = config.project.npmScope;
|
||||
const projects: ProjectNode[] = getProjectNodes(config);
|
||||
|
||||
// fetch all apps and libs
|
||||
const deps = dependencies(npmScope, projects, f =>
|
||||
readFileSync(`${appRoot.path}/${f}`, 'utf-8')
|
||||
);
|
||||
|
||||
return {
|
||||
deps,
|
||||
criticalPath
|
||||
};
|
||||
}
|
||||
|
||||
function getDot(json: JSONOutput) {
|
||||
const config = readCliConfig();
|
||||
const projects: ProjectNode[] = getProjectNodes(config);
|
||||
|
||||
return createGraphviz(
|
||||
graphvizConfig,
|
||||
json.deps,
|
||||
projects,
|
||||
json.criticalPath.reduce((m, proj) => ({ ...m, [proj]: true }), {})
|
||||
);
|
||||
}
|
||||
|
||||
function getConfigFromUserInput(cmdOpts: UserOptions): ParsedUserOptions {
|
||||
const filename = cmdOpts.file;
|
||||
const output = cmdOpts.output;
|
||||
|
||||
if (filename && output) {
|
||||
throw new Error(
|
||||
'Received both filename as well as output type. Please only specify one of the options.'
|
||||
);
|
||||
}
|
||||
|
||||
const extension = !!filename
|
||||
? path.extname(filename).substring(1)
|
||||
: output || OutputType.html;
|
||||
return {
|
||||
isFilePresent: !output,
|
||||
type: extension,
|
||||
output: output,
|
||||
shouldOpen: !output && !filename,
|
||||
filename
|
||||
};
|
||||
}
|
||||
|
||||
function extractDataFromJson(json, type) {
|
||||
switch (type) {
|
||||
case OutputType.json:
|
||||
return JSON.stringify(json, null, 2);
|
||||
case OutputType.dot:
|
||||
return getDot(json);
|
||||
case OutputType.html:
|
||||
return applyHTMLTemplate(viz(getDot(json)));
|
||||
default:
|
||||
throw new Error(
|
||||
'Unrecognized file extension. Supported extensions are "json", "html", and "dot"'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateGraph(
|
||||
args: UserOptions,
|
||||
criticalPath?: string[]
|
||||
): void {
|
||||
const json = generateGraphJson(criticalPath || []);
|
||||
|
||||
const config = {
|
||||
...defaultConfig,
|
||||
...getConfigFromUserInput(args)
|
||||
};
|
||||
|
||||
handleOutput({
|
||||
data: extractDataFromJson(json, config.type),
|
||||
filename: config.filename,
|
||||
shouldWriteToFile: config.isFilePresent,
|
||||
shouldOpen: config.shouldOpen
|
||||
});
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { update } from './update';
|
||||
import { patchNg } from './patch-ng';
|
||||
import { lint } from './lint';
|
||||
import { workspaceSchematic } from './workspace-schematic';
|
||||
import { generateGraph } from './dep-graph';
|
||||
|
||||
const processedArgs = yargsParser(process.argv, {
|
||||
alias: {
|
||||
@ -21,6 +22,9 @@ switch (command) {
|
||||
case 'affected':
|
||||
affected(args);
|
||||
break;
|
||||
case 'dep-graph':
|
||||
generateGraph(yargsParser(args));
|
||||
break;
|
||||
case 'format':
|
||||
format(args);
|
||||
break;
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { execSync } from 'child_process';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
affectedApps,
|
||||
AffectedFetcher,
|
||||
affectedAppNames,
|
||||
ProjectNode,
|
||||
ProjectType,
|
||||
touchedProjects
|
||||
touchedProjects,
|
||||
dependencies,
|
||||
Dependency,
|
||||
affectedProjectNames
|
||||
} from './affected-apps';
|
||||
import * as fs from 'fs';
|
||||
import {
|
||||
dependencies,
|
||||
Dependency
|
||||
} from '@nrwl/schematics/src/command-line/affected-apps';
|
||||
import { statSync } from 'fs';
|
||||
import * as appRoot from 'app-root-path';
|
||||
import { readJsonFile } from '../utils/fileutils';
|
||||
@ -110,17 +110,22 @@ export function readCliConfig(): any {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function getAffectedApps(touchedFiles: string[]): string[] {
|
||||
export const getAffected = (affectedNamesFetcher: AffectedFetcher) => (
|
||||
touchedFiles: string[]
|
||||
): string[] => {
|
||||
const config = readCliConfig();
|
||||
const projects = getProjectNodes(config);
|
||||
|
||||
return affectedApps(
|
||||
return affectedNamesFetcher(
|
||||
config.project.npmScope,
|
||||
projects,
|
||||
f => fs.readFileSync(`${appRoot.path}/${f}`, 'utf-8'),
|
||||
touchedFiles
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAffectedApps = getAffected(affectedAppNames);
|
||||
export const getAffectedProjects = getAffected(affectedProjectNames);
|
||||
|
||||
export function getTouchedProjects(touchedFiles: string[]): string[] {
|
||||
return touchedProjects(getProjectNodes(readCliConfig()), touchedFiles).filter(
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export function writeToFile(path: string, str: string) {
|
||||
fs.writeFileSync(path, str);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is specifically for updating a JSON file using the filesystem
|
||||
*
|
||||
@ -12,7 +16,7 @@ import * as path from 'path';
|
||||
export function updateJsonFile(path: string, callback: (a: any) => any) {
|
||||
const json = readJsonFile(path);
|
||||
callback(json);
|
||||
fs.writeFileSync(path, serializeJson(json));
|
||||
writeToFile(path, serializeJson(json));
|
||||
}
|
||||
|
||||
export function addApp(apps: any[] | undefined, newApp: any): any[] {
|
||||
@ -58,3 +62,17 @@ export function copyFile(file: string, target: string) {
|
||||
source.pipe(dest);
|
||||
source.on('error', e => console.error(e));
|
||||
}
|
||||
|
||||
function directoryExists(name) {
|
||||
try {
|
||||
return fs.statSync(name).isDirectory();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function createDirectory(name: string) {
|
||||
if (!directoryExists(name)) {
|
||||
fs.mkdirSync(name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
npm run build
|
||||
./scripts/link.sh
|
||||
rm -rf tmp
|
||||
mkdir tmp
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@ -3017,6 +3017,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6:
|
||||
version "4.1.11"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
|
||||
|
||||
graphviz@^0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/graphviz/-/graphviz-0.0.8.tgz#e599e40733ef80e1653bfe89a5f031ecf2aa4aaa"
|
||||
dependencies:
|
||||
temp "~0.4.0"
|
||||
|
||||
growly@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
||||
@ -5263,7 +5269,7 @@ onetime@^2.0.0:
|
||||
dependencies:
|
||||
mimic-fn "^1.0.0"
|
||||
|
||||
opn@^5.1.0:
|
||||
opn@^5.1.0, opn@^5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
|
||||
dependencies:
|
||||
@ -7153,6 +7159,10 @@ tar@^2.0.0, tar@^2.2.1:
|
||||
fstream "^1.0.2"
|
||||
inherits "2"
|
||||
|
||||
temp@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/temp/-/temp-0.4.0.tgz#671ad63d57be0fe9d7294664b3fc400636678a60"
|
||||
|
||||
term-size@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
|
||||
@ -7615,6 +7625,10 @@ verror@1.10.0:
|
||||
core-util-is "1.0.2"
|
||||
extsprintf "^1.2.0"
|
||||
|
||||
viz.js@^1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/viz.js/-/viz.js-1.8.1.tgz#277ab3cf4367c608a95b281a7472083c3e2ee6cf"
|
||||
|
||||
vlq@^0.2.1, vlq@^0.2.2:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user