@datapith/cdk4j-diagram 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BUILD.md ADDED
@@ -0,0 +1,12 @@
1
+ ### Development Build on Windows
2
+
3
+ 1. Install npm from https://github.com/coreybutler/nvm-windows#overview
4
+ 2. nvm ls
5
+ 3. nvm use 16.13.1
6
+ 4. npm install -g pkg
7
+ 6. npm install
8
+ 7. pkg index.js -o cfn-dia
9
+
10
+ ### Running from node
11
+ node index.js h -t tests/template.json
12
+ cd ta
@@ -0,0 +1,58 @@
1
+ const program = require("commander");
2
+ const mxGenerator = require("../../graph/MxGenerator");
3
+ const templateParser = require("../../shared/templateParser");
4
+ const filterConfig = require("../../resources/FilterConfig");
5
+ const mxGraphToAsciiArt = require("./mxgraph-to-asciiart");
6
+ const fs = require("fs");
7
+ program
8
+ .command("ascii-art")
9
+ .alias("a")
10
+ .option(
11
+ "-t, --template-file [templateFile]",
12
+ "Path to template or cdk.json file",
13
+ "template.yaml or cdk.json"
14
+ )
15
+ .option(
16
+ "--stacks [stacks]",
17
+ "Comma separated list of stack name(s) to include. Defaults to all."
18
+ )
19
+ .option("-co, --cdk-output [outputPath]", "CDK synth output path", `cdk.out`)
20
+ .option("-s, --skip-synth", "Skips CDK synth", false)
21
+ .option(
22
+ "-w, --watch",
23
+ "Watch for changes in template and rerender diagram on change",
24
+ false
25
+ )
26
+ .option(
27
+ "-e, --exclude-types [excludeTypes...]",
28
+ "List of resource types to exclude when using CI mode"
29
+ )
30
+ .description("Generates an ascii-art diagram from a CloudFormation template")
31
+ .action(async (cmd) => {
32
+ const xml = await render(cmd);
33
+ if (cmd.watch) {
34
+ fs.watchFile(cmd.templateFile, (a, b) => {
35
+ render(cmd);
36
+ });
37
+ }
38
+ });
39
+ async function render(cmd) {
40
+ try {
41
+ const template = templateParser.get(cmd).template;
42
+
43
+ if (!template.Resources) {
44
+ console.log("Can't find resources");
45
+ return;
46
+ }
47
+ filterConfig.resourceNamesToInclude = Object.keys(template.Resources);
48
+ filterConfig.resourceTypesToInclude = Object.keys(template.Resources).map(
49
+ (p) => template.Resources[p].Type
50
+ );
51
+ mxGenerator.reset();
52
+ const xml = await mxGenerator.renderTemplate(template);
53
+ mxGraphToAsciiArt.render(xml);
54
+ return xml;
55
+ } catch (err) {
56
+ console.log(err.message);
57
+ }
58
+ }
@@ -0,0 +1,203 @@
1
+ const clc = require("cli-color");
2
+ const parser = require("fast-xml-parser");
3
+ const colorConvert = require("color-convert");
4
+ const yScale = 12;
5
+ const xScale = 7;
6
+ const serviceColors = {
7
+ lambda: clc.bgYellow,
8
+ apigateway: clc.bgRed,
9
+ sqs: clc.bgRed,
10
+ sns: clc.bgRed,
11
+ dynamodb: clc.bgBlue,
12
+ };
13
+
14
+ let highestY = 0;
15
+ function render(xml) {
16
+ const doc = parser.parse(xml, {
17
+ ignoreAttributes: false,
18
+ attributeNamePrefix: "",
19
+ });
20
+
21
+ console.clear();
22
+ const mxGraphModel = doc.mxGraphModel || doc.mxfile.diagram.mxGraphModel;
23
+ for (const cell of mxGraphModel.root.mxCell) {
24
+ if (
25
+ cell.mxGeometry &&
26
+ cell.mxGeometry.Array &&
27
+ cell.mxGeometry.Array.as === "points"
28
+ ) {
29
+ let lastKnownX, lastKnownY;
30
+ const color = Math.floor(Math.random() * 200 + 100);
31
+ for (let i = 0; i < cell.mxGeometry.Array.mxPoint.length; i++) {
32
+ const point = cell.mxGeometry.Array.mxPoint[i];
33
+ if (lastKnownX && lastKnownY) {
34
+ edge(
35
+ lastKnownX / xScale,
36
+ lastKnownY / yScale,
37
+ (point.x || lastKnownX) / xScale,
38
+ (point.y || lastKnownY) / yScale,
39
+ color,
40
+ i === cell.mxGeometry.Array.mxPoint.length - 1
41
+ );
42
+ }
43
+ lastKnownX = point.x || lastKnownX;
44
+ lastKnownY = point.y || lastKnownY;
45
+ highestY = Math.max(highestY, lastKnownY);
46
+ }
47
+ }
48
+ }
49
+
50
+ for (const cell of mxGraphModel.root.mxCell) {
51
+ if (cell.mxGeometry && cell.mxGeometry.width) {
52
+ const resourceType = cell.style.match(/resIcon=mxgraph.aws4.(.+);/);
53
+ const color = cell.style.match(/fillColor=#(.+?);/);
54
+ box(
55
+ cell.mxGeometry.x / xScale,
56
+ cell.mxGeometry.y / yScale,
57
+ resourceType ? resourceType[1] : "",
58
+ cell.value,
59
+ color ? color[1] : "#000000"
60
+ );
61
+ highestY = Math.max(cell.mxGeometry.y + 4, highestY);
62
+ }
63
+ }
64
+ process.stdout.write(clc.move.bottom);
65
+ process.stdout.write(clc.move.lineBegin);
66
+ }
67
+
68
+ function edge(fromX, fromY, toX, toY, color, isLast) {
69
+ const coloredLine = clc.xterm(color);
70
+ let x = fromX;
71
+ let y = fromY;
72
+ const deltaX = Math.max(Math.min(1, (toX - fromX) / xScale), -1);
73
+ const deltaY = Math.max(Math.min(1, (toY - fromY) / yScale), -1);
74
+ let prevX = Math.floor(x);
75
+ let prevY = Math.floor(y);
76
+ let safetyExit = 0;
77
+ while (
78
+ (Math.floor(x) !== Math.floor(toX) || Math.floor(y) !== Math.floor(toY)) &&
79
+ safetyExit++ < 50
80
+ ) {
81
+ const flooredX = Math.floor(x);
82
+ const flooredY = Math.floor(y);
83
+ if (flooredX !== Math.floor(toX)) x = x + deltaX;
84
+ if (flooredY !== Math.floor(toY)) y = y + deltaY;
85
+ let char = "+";
86
+ if (flooredY === prevY && flooredX !== prevX) char = "─";
87
+ if (flooredY === prevY && flooredX === prevX) char = "─";
88
+ if (flooredY > prevY) {
89
+ if (flooredX == prevX) char = "│";
90
+ if (flooredX > prevX) {
91
+ char = "└";
92
+ moveToAbsolute(flooredX, flooredY - 1);
93
+ process.stdout.write(coloredLine("┐"));
94
+ }
95
+ if (flooredX < prevX) {
96
+ char = "┘";
97
+ moveToAbsolute(flooredX, flooredY - 1);
98
+ process.stdout.write(coloredLine("┌"));
99
+ }
100
+ }
101
+ if (flooredY < prevY) {
102
+ if (flooredX === prevX) char = "│";
103
+ if (flooredX > prevX) {
104
+ char = "┌";
105
+ moveToAbsolute(flooredX, flooredY + 1);
106
+ process.stdout.write(coloredLine("┘"));
107
+ }
108
+ if (flooredX < prevX) {
109
+ char = "┐";
110
+ moveToAbsolute(flooredX, flooredY + 1);
111
+ process.stdout.write(coloredLine("└"));
112
+ }
113
+ }
114
+ prevX = flooredX;
115
+ prevY = flooredY;
116
+ moveToAbsolute(flooredX, flooredY);
117
+ process.stdout.write(coloredLine(char));
118
+ }
119
+ if (isLast) {
120
+ moveToAbsolute(toX, toY);
121
+ if (fromX > toX) process.stdout.write(coloredLine("<"));
122
+ else process.stdout.write(coloredLine(">"));
123
+ }
124
+ }
125
+
126
+ function box(x, y, type, text, color) {
127
+ const coloredBox =
128
+ clc.bgXterm(rgbToX256(...colorConvert.hex.rgb(color))) || clc.bgBlack;
129
+
130
+ y = y || 0;
131
+ x = x || 0;
132
+ const height = 2;
133
+ if (text.length > 20) {
134
+ text = text.substring(0, 20) + "...";
135
+ }
136
+ const width = Math.max(text.length, type.length + 2) + 2;
137
+ moveToAbsolute(x, y);
138
+ print(coloredBox, `┌${"─".repeat(width - 2)}╮`);
139
+ for (let row = 0; row < height; row++) {
140
+ moveToRelative(-width, 1);
141
+ print(coloredBox, `│${" ".repeat(width - 2)}│`);
142
+ }
143
+ moveToRelative(-width, 1);
144
+ print(coloredBox, `╰${"─".repeat(width - 2)}╯`);
145
+ moveToAbsolute(x + 1, y + Math.floor(height / 2));
146
+ print(coloredBox, text);
147
+ moveToAbsolute(x + 1, y + Math.floor(height / 2) + 1);
148
+ print(coloredBox, `(${type})`);
149
+ }
150
+
151
+ function print(coloredBox, text) {
152
+ process.stdout.write(coloredBox(text));
153
+ }
154
+
155
+ function moveToAbsolute(x, y) {
156
+ process.stdout.write(clc.move.to(x, y + 7));
157
+ }
158
+
159
+ function moveToRelative(x, y) {
160
+ process.stdout.write(clc.move(x, y));
161
+ }
162
+
163
+ function rgbToX256(r, g, b) {
164
+ // Calculate the nearest 0-based color index at 16 .. 231
165
+ const v2ci = (v) => {
166
+ if (v < 48) {
167
+ return 0;
168
+ } else if (v < 115) {
169
+ return 1;
170
+ } else {
171
+ return Math.trunc((v - 35) / 40);
172
+ }
173
+ };
174
+
175
+ const ir = v2ci(r);
176
+ const ig = v2ci(g);
177
+ const ib = v2ci(b);
178
+ const colorIndex = 36 * ir + 6 * ig + ib;
179
+
180
+ // Calculate the nearest 0-based gray index at 232 .. 255
181
+ const average = Math.trunc((r + g + b) / 3);
182
+ const grayIndex = average > 238 ? 23 : Math.trunc((average - 3) / 10);
183
+
184
+ // Calculate the represented colors back from the index
185
+ const i2cv = [0, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
186
+ const cr = i2cv[ir];
187
+ const cg = i2cv[ig];
188
+ const cb = i2cv[ib];
189
+ const gv = 8 + 10 * grayIndex;
190
+
191
+ // Return the one which is nearer to the original input rgb value
192
+ const distSquare = (A, B, C, a, b, c) => {
193
+ return (A - a) * (A - a) + (B - b) * (B - b) + (C - c) * (C - c);
194
+ };
195
+ const colorErr = distSquare(cr, cg, cb, r, g, b);
196
+ const grayErr = distSquare(gv, gv, gv, r, g, b);
197
+
198
+ return colorErr <= grayErr ? 16 + colorIndex : 232 + grayIndex;
199
+ }
200
+
201
+ module.exports = {
202
+ render,
203
+ };
@@ -0,0 +1,61 @@
1
+ const program = require("commander");
2
+ const Vis = require("../../graph/Vis");
3
+ const draw = require("../../graph/MxGenerator");
4
+ const { CloudFormationClient, ListStacksCommand, GetTemplateCommand } = require("@aws-sdk/client-cloudformation");
5
+ const inquirer = require("inquirer");
6
+ const prompt = inquirer.createPromptModule();
7
+ const YAML = require("yaml-cfn");
8
+ const jsonUtil = require("../../resources/JsonUtil");
9
+ const { fromSSO } = require("@aws-sdk/credential-provider-sso");
10
+
11
+ program
12
+ .command("browse")
13
+ .alias("b")
14
+ .option("-o --output [output]", "Output format. draw.io or html")
15
+ .option("--output-file [outputFile]", "Output file. Only used when using draw.io output. Defaults to <stack-name>.drawio")
16
+ .option("-p --profile [profile]", "AWS CLI profile")
17
+ .option("-r --region [region]", "AWS region")
18
+ .description("Browses and generates diagrams from your deployed templates")
19
+ .action(async (cmd) => {
20
+
21
+ const credentials = await fromSSO({ profile: cmd.profile || 'default' })();
22
+
23
+ const cloudFormation = new CloudFormationClient({ credentials, region: cmd.region || process.env.AWS_REGION });
24
+
25
+ let nextToken = null;
26
+ const stackList = [];
27
+ do {
28
+ const response = await cloudFormation.send(new ListStacksCommand({
29
+ NextToken: nextToken,
30
+ StackStatusFilter: ["CREATE_COMPLETE", "UPDATE_COMPLETE"],
31
+ }));
32
+ stackList.push(...response.StackSummaries.map((p) => p.StackName));
33
+ nextToken = response.NextToken;
34
+ } while (nextToken);
35
+ while (true) {
36
+ const stack = await prompt({
37
+ name: "stackName",
38
+ type: "list",
39
+ choices: stackList.sort(),
40
+ });
41
+ const templateBody = (
42
+ await cloudFormation.send(new GetTemplateCommand({ StackName: stack.stackName }))
43
+ ).TemplateBody;
44
+ const isJson = jsonUtil.isJson(templateBody);
45
+ const parser = isJson ? JSON.parse : YAML.yamlParse;
46
+
47
+ const template = parser(templateBody);
48
+ if (cmd.output === "html") {
49
+ await Vis.renderTemplate(
50
+ template,
51
+ template.isJson,
52
+ cmd.outputPath,
53
+ false,
54
+ true
55
+ );
56
+ } else {
57
+ cmd.outputFile = cmd.outputFile || stack.stackName + ".drawio"
58
+ await draw.generate(cmd, template);
59
+ }
60
+ }
61
+ });
@@ -0,0 +1,40 @@
1
+ const program = require("commander");
2
+ const mxGenerator = require("../../graph/MxGenerator");
3
+
4
+ let ciMode = false;
5
+
6
+ program
7
+ .command("draw.io")
8
+ .alias("d")
9
+ .option(
10
+ "-t, --template-file [templateFile]",
11
+ "Path to template or cdk.json file",
12
+ "template.yaml or cdk.json"
13
+ )
14
+ .option(
15
+ "-c, --ci-mode",
16
+ "Disable terminal/console interactivity",
17
+ false
18
+ )
19
+ .option("--stacks [stacks]", "Comma separated list of stack name(s) to include. Defaults to all.")
20
+ .option("-o, --output-file [outputFile]", "Name of output file", "template.drawio")
21
+ .option(
22
+ "-co, --cdk-output [outputPath]",
23
+ "CDK synth output path",
24
+ `cdk.out`
25
+ )
26
+ .option(
27
+ "-s, --skip-synth",
28
+ "Skips CDK synth",
29
+ false
30
+ )
31
+ .option(
32
+ "-e, --exclude-types [excludeTypes...]",
33
+ "List of resource types to exclude when using CI mode"
34
+ )
35
+ .description("Generates a draw.io diagram from a CloudFormation template")
36
+ .action(async (cmd) => {
37
+ await mxGenerator.generate(cmd);
38
+ });
39
+
40
+
@@ -0,0 +1,40 @@
1
+ const program = require("commander");
2
+ const template = require("../../shared/templateParser");
3
+ const path = require("path");
4
+ const tempDirectory = require("temp-dir");
5
+ const Vis = require("../../graph/Vis");
6
+ const YAML = require("yaml-cfn");
7
+
8
+ program
9
+ .command("html")
10
+ .alias("h")
11
+ .option(
12
+ "-t, --template-file [templateFile]",
13
+ "Path to template or cdk.json file",
14
+ "template.yaml or cdk.json"
15
+ )
16
+ .option("--stacks [stacks]", "Comma separated list of stack name(s) to include. Defaults to all.")
17
+ .option("-all --render-all", "If set, all nested stacks will be rendered. By default only root template is rendered", false)
18
+ .option("-c, --ci-mode", "Disable terminal/console interactivity", false)
19
+ .option(
20
+ "-o, --output-path [outputPath]",
21
+ "Name of output file",
22
+ `${path.join(tempDirectory, "cdk4j-diagram")}`
23
+ )
24
+ .option("-co, --cdk-output [outputPath]", "CDK synth output path", `cdk.out`)
25
+ .option("-s, --skip-synth", "Skips CDK synth", false)
26
+ .option("-sa, --standalone", "Creates a self-contained standalone index.html", false)
27
+ .description("Generates a vis.js diagram from a CloudFormation template")
28
+ .action(async (cmd) => {
29
+ ciMode = cmd.ciMode;
30
+ const templateObj = template.get(cmd);
31
+ await Vis.renderTemplate(
32
+ templateObj.template,
33
+ template.isJson,
34
+ cmd.outputPath,
35
+ true,
36
+ false,
37
+ cmd.standalone,
38
+ cmd.renderAll
39
+ );
40
+ });
@@ -0,0 +1,100 @@
1
+ const Vis = require("../../graph/Vis");
2
+ const program = require("commander");
3
+ const template = require("../../shared/templateParser");
4
+ const fs = require("fs");
5
+ program
6
+ .command("mermaid")
7
+ .alias("m")
8
+ .option(
9
+ "-t, --template-file [templateFile]",
10
+ "Path to template or cdk.json file",
11
+ "template.yaml or cdk.json"
12
+ )
13
+ .option("-all --render-all", "If set, all nested stacks will be rendered. By default only root template is rendered", false)
14
+ .option(
15
+ "-o, --output-path [outputPath]",
16
+ "Name of output file"
17
+ )
18
+ .option("-co, --cdk-output [cdkOutputPath]", "CDK synth output path", `cdk.out`)
19
+ .option("-s, --skip-synth", "Skips CDK synth", false)
20
+ .description("Generates a mermaid graph from a template")
21
+ .action(async (cmd) => {
22
+ ciMode = cmd.ciMode;
23
+ const templateObj = template.get(cmd);
24
+ const graph = await Vis.makeGraph(
25
+ templateObj.template,
26
+ "root",
27
+ false,
28
+ cmd.renderAll
29
+ );
30
+
31
+ const groups = {};
32
+ for (const edge of graph.edges) {
33
+ const owner = edge.from.split(".")[0];
34
+
35
+ if (edge.to.startsWith(`${owner}.`) && edge.from.startsWith(`${owner}.`)) {
36
+ if (!groups[owner]) {
37
+ groups[owner] = [];
38
+ }
39
+ groups[owner].push(edge);
40
+ } else {
41
+ if (!groups["crossgroup"]) {
42
+ groups["crossgroup"] = [];
43
+ }
44
+ groups["crossgroup"].push(edge);
45
+ }
46
+ }
47
+ const uniqueRelations = [];
48
+ let mermaidString = `\`\`\`mermaid\n\tflowchart TB;\n`;
49
+ for (const groupKey in groups) {
50
+ const group = groups[groupKey];
51
+ if (groupKey !== "crossgroup") {
52
+ mermaidString += `\t\tsubgraph ${groupKey !== "root" ? groupKey : "&nbsp;"}\n`;
53
+ }
54
+
55
+ mermaidString += `${group.map(p => {
56
+ const fromResource = graph.nodes.find(n => n.id === p.from);
57
+ const toResource = graph.nodes.find(n => n.id === p.to);
58
+
59
+ const from = createShape(fromResource);
60
+ const to = createShape(toResource);
61
+ const relation = `\t\t${from}-->${to}`;
62
+ if (!uniqueRelations.includes(relation)) {
63
+ uniqueRelations.push(relation);
64
+ return relation;
65
+ }
66
+ }).filter(p => p).join("\n")}
67
+
68
+ `
69
+ if (groupKey !== "crossgroup") {
70
+ mermaidString += `\tend\n`;
71
+ }
72
+
73
+ }
74
+
75
+ mermaidString += `\n\`\`\``;
76
+ if (cmd.outputPath) {
77
+ fs.writeFileSync(cmd.outputPath, mermaidString);
78
+ console.log(`Wrote Mermaid diagram to ${cmd.outputPath}`);
79
+ } else {
80
+ console.log(mermaidString)
81
+ }
82
+ });
83
+
84
+ function createShape(resource, cmd) {
85
+ const label = resource.label.replace(/[^a-z0-9\n]/gmi, "").replace(/\s+/g, "");
86
+ const id = resource.id.replace(/[^a-z0-9\n]/gmi, "").replace(/\s+/g, "");;
87
+ const type = resource.type.replace("AWS::", "");
88
+ switch (resource.type) {
89
+ case "AWS::Serverless::Function":
90
+ case "AWS::Lambda::Function":
91
+ return `${id}[[${label}<br/>${type}]]`;
92
+ case "AWS::Serverless::SimpleTable":
93
+ case "AWS::DynamoDB::Table":
94
+ case "AWS::RDS::DBInstance":
95
+ case "AWS::RDS::DBCluster":
96
+ return `${id}[(${label}<br/>${type})]`;
97
+ }
98
+ return `${id}[${label}<br/>${type}]`;
99
+
100
+ }