UNPKG

3.69 kBJavaScriptView Raw
1'use strict';
2
3const path = require('path');
4const pify = require('pify');
5const graphviz = require('graphviz');
6
7const exec = pify(require('child_process').exec);
8const writeFile = pify(require('fs').writeFile);
9
10/**
11 * Set color on a node.
12 * @param {Object} node
13 * @param {String} color
14 */
15function setNodeColor(node, color) {
16 node.set('color', color);
17 node.set('fontcolor', color);
18}
19
20/**
21 * Check if Graphviz is installed on the system.
22 * @param {Object} config
23 * @return {Promise}
24 */
25function checkGraphvizInstalled(config) {
26 if (config.graphVizPath) {
27 const cmd = path.join(config.graphVizPath, 'gvpr -V');
28 return exec(cmd)
29 .catch(() => {
30 throw new Error('Could not execute ' + cmd);
31 });
32 }
33
34 return exec('gvpr -V')
35 .catch((error) => {
36 throw new Error('Graphviz could not be found. Ensure that "gvpr" is in your $PATH.\n' + error);
37 });
38}
39
40/**
41 * Return options to use with graphviz digraph.
42 * @param {Object} config
43 * @return {Object}
44 */
45function createGraphvizOptions(config) {
46 const graphVizOptions = config.graphVizOptions || {};
47
48 return {
49 G: Object.assign({
50 overlap: false,
51 pad: 0.111,
52 layout: config.layout,
53 bgcolor: config.backgroundColor
54 }, graphVizOptions.G),
55 E: Object.assign({
56 color: config.edgeColor
57 }, graphVizOptions.E),
58 N: Object.assign({
59 fontname: config.fontName,
60 fontsize: config.fontSize,
61 color: config.nodeColor,
62 fontcolor: config.nodeColor
63 }, graphVizOptions.N)
64 };
65}
66
67/**
68 * Creates the graphviz graph.
69 * @param {Object} modules
70 * @param {Array} circular
71 * @param {Object} config
72 * @param {Object} options
73 * @return {Promise}
74 */
75function createGraph(modules, circular, config, options) {
76 const g = graphviz.digraph('G');
77 const nodes = {};
78 const cyclicModules = circular.reduce((a, b) => a.concat(b), []);
79
80 if (config.graphVizPath) {
81 g.setGraphVizPath(config.graphVizPath);
82 }
83
84 Object.keys(modules).forEach((id) => {
85 nodes[id] = nodes[id] || g.addNode(id);
86
87 if (!modules[id].length) {
88 setNodeColor(nodes[id], config.noDependencyColor);
89 } else if (cyclicModules.indexOf(id) >= 0) {
90 setNodeColor(nodes[id], config.cyclicNodeColor);
91 }
92
93 modules[id].forEach((depId) => {
94 nodes[depId] = nodes[depId] || g.addNode(depId);
95
96 if (!modules[depId]) {
97 setNodeColor(nodes[depId], config.noDependencyColor);
98 }
99
100 g.addEdge(nodes[id], nodes[depId]);
101 });
102 });
103
104 return new Promise((resolve, reject) => {
105 g.output(options, resolve, (code, out, err) => {
106 reject(new Error(err));
107 });
108 });
109}
110
111/**
112 * Creates an image from the module dependency graph.
113 * @param {Object} modules
114 * @param {Array} circular
115 * @param {String} imagePath
116 * @param {Object} config
117 * @return {Promise}
118 */
119module.exports.image = function (modules, circular, imagePath, config) {
120 const options = createGraphvizOptions(config);
121
122 options.type = path.extname(imagePath).replace('.', '') || 'png';
123
124 return checkGraphvizInstalled(config)
125 .then(() => {
126 return createGraph(modules, circular, config, options)
127 .then((image) => writeFile(imagePath, image))
128 .then(() => path.resolve(imagePath));
129 });
130};
131
132/**
133 * Return the module dependency graph as DOT output.
134 * @param {Object} modules
135 * @param {Object} config
136 * @return {Promise}
137 */
138module.exports.dot = function (modules, config) {
139 const nodes = {};
140 const g = graphviz.digraph('G');
141
142 return checkGraphvizInstalled(config)
143 .then(() => {
144 Object.keys(modules).forEach((id) => {
145 nodes[id] = nodes[id] || g.addNode(id);
146
147 modules[id].forEach((depId) => {
148 nodes[depId] = nodes[depId] || g.addNode(depId);
149 g.addEdge(nodes[id], nodes[depId]);
150 });
151 });
152
153 return g.to_dot();
154 });
155};