@webpieces/nx-webpieces-rules 0.4.463 → 0.4.464

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.463",
3
+ "version": "0.4.464",
4
4
  "description": "Nx-specific webpieces validation rules and graph tooling. Bundles all @webpieces rule packages with Nx graph validators and an inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -21,11 +21,11 @@
21
21
  "README.md"
22
22
  ],
23
23
  "dependencies": {
24
- "@webpieces/ai-hook-rules": "0.4.463",
25
- "@webpieces/code-rules": "0.4.463",
26
- "@webpieces/eslint-rules": "0.4.463",
27
- "@webpieces/pr-gate": "0.4.463",
28
- "@webpieces/rules-config": "0.4.463",
24
+ "@webpieces/ai-hook-rules": "0.4.464",
25
+ "@webpieces/code-rules": "0.4.464",
26
+ "@webpieces/eslint-rules": "0.4.464",
27
+ "@webpieces/pr-gate": "0.4.464",
28
+ "@webpieces/rules-config": "0.4.464",
29
29
  "madge": "8.0.0"
30
30
  },
31
31
  "peerDependencies": {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * DOT syntax helpers
3
+ *
4
+ * Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:
5
+ *
6
+ * 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)
7
+ * becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how
8
+ * an unescaped `"` shipped and took the whole diagram down: in DOT a bare `"` TERMINATES the
9
+ * string it appears in, so one bad node line makes the entire graph fail to parse.
10
+ * 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of
11
+ * mistake into a thrown error at generation time, instead of a blank page with a Graphviz
12
+ * "syntax error in line N" that only a human opening the HTML ever sees.
13
+ */
14
+ /**
15
+ * Escape a runtime value for use INSIDE a quoted DOT string.
16
+ *
17
+ * Only `\` and `"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once
18
+ * it is inside quotes. Note this deliberately escapes `\` FIRST, so a value containing a backslash
19
+ * cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\n` AFTER
20
+ * escaping their values — the separator is ours, the value is theirs.
21
+ */
22
+ export declare function dotValue(value: string): string;
23
+ /** Thrown when the generator produces DOT that Graphviz could not parse. */
24
+ export declare class InvalidDotError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ /**
28
+ * Fail loudly on structurally broken DOT.
29
+ *
30
+ * This is not a full Graphviz parser — it is the check that catches the failure mode a string
31
+ * builder actually has: a quote that ends a string early (or never ends it). It scans the quoted
32
+ * strings honouring `\"` escapes and asserts each one is terminated and is bounded by DOT
33
+ * punctuation rather than by bare text. An unescaped `"` inside a label always violates that: the
34
+ * string ends mid-label, and the remaining label text becomes stray tokens.
35
+ */
36
+ export declare function assertValidDot(dot: string, source: string): void;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * DOT syntax helpers
4
+ *
5
+ * Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:
6
+ *
7
+ * 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)
8
+ * becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how
9
+ * an unescaped `"` shipped and took the whole diagram down: in DOT a bare `"` TERMINATES the
10
+ * string it appears in, so one bad node line makes the entire graph fail to parse.
11
+ * 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of
12
+ * mistake into a thrown error at generation time, instead of a blank page with a Graphviz
13
+ * "syntax error in line N" that only a human opening the HTML ever sees.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.InvalidDotError = void 0;
17
+ exports.dotValue = dotValue;
18
+ exports.assertValidDot = assertValidDot;
19
+ /** Chars a quoted string may legally sit directly after, ignoring whitespace. */
20
+ const LEGAL_BEFORE_STRING = new Set(['=', '[', ',', ';', '{', '}', '>', '-']);
21
+ /** Chars a quoted string may legally be followed by, ignoring whitespace. */
22
+ const LEGAL_AFTER_STRING = new Set(['=', '[', ']', ',', ';', '{', '}', '-', '>']);
23
+ /**
24
+ * Escape a runtime value for use INSIDE a quoted DOT string.
25
+ *
26
+ * Only `\` and `"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once
27
+ * it is inside quotes. Note this deliberately escapes `\` FIRST, so a value containing a backslash
28
+ * cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\n` AFTER
29
+ * escaping their values — the separator is ours, the value is theirs.
30
+ */
31
+ // webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
32
+ function dotValue(value) {
33
+ return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
34
+ }
35
+ /** Thrown when the generator produces DOT that Graphviz could not parse. */
36
+ class InvalidDotError extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = 'InvalidDotError';
40
+ }
41
+ }
42
+ exports.InvalidDotError = InvalidDotError;
43
+ /**
44
+ * Fail loudly on structurally broken DOT.
45
+ *
46
+ * This is not a full Graphviz parser — it is the check that catches the failure mode a string
47
+ * builder actually has: a quote that ends a string early (or never ends it). It scans the quoted
48
+ * strings honouring `\"` escapes and asserts each one is terminated and is bounded by DOT
49
+ * punctuation rather than by bare text. An unescaped `"` inside a label always violates that: the
50
+ * string ends mid-label, and the remaining label text becomes stray tokens.
51
+ */
52
+ // webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
53
+ function assertValidDot(dot, source) {
54
+ // Comment text is not code: a `"` or a word in it must never be read as a DOT token. Blanking
55
+ // it (offsets preserved) keeps every reported line number the one Graphviz would report.
56
+ const code = blankComments(dot);
57
+ let index = 0;
58
+ while (index < code.length) {
59
+ if (code[index] !== '"') {
60
+ index++;
61
+ continue;
62
+ }
63
+ const start = index;
64
+ index++;
65
+ while (index < code.length && code[index] !== '"') {
66
+ index += code[index] === '\\' ? 2 : 1;
67
+ }
68
+ if (index >= code.length) {
69
+ throw new InvalidDotError(`${source}: unterminated string starting at ${describe(dot, start)}`);
70
+ }
71
+ const end = index;
72
+ index++;
73
+ checkNeighbor(code, dot, start, end, source);
74
+ }
75
+ }
76
+ /** Replace `//`, `#` and `/* *\/` comment bodies with spaces, preserving length and newlines. */
77
+ // webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
78
+ function blankComments(dot) {
79
+ const out = dot.split('');
80
+ let index = 0;
81
+ let inString = false;
82
+ while (index < out.length) {
83
+ const two = dot.slice(index, index + 2);
84
+ if (inString) {
85
+ if (dot[index] === '\\')
86
+ index++;
87
+ else if (dot[index] === '"')
88
+ inString = false;
89
+ index++;
90
+ }
91
+ else if (dot[index] === '"') {
92
+ inString = true;
93
+ index++;
94
+ }
95
+ else if (two === '//' || dot[index] === '#') {
96
+ while (index < out.length && out[index] !== '\n')
97
+ out[index++] = ' ';
98
+ }
99
+ else if (two === '/*') {
100
+ while (index < out.length && dot.slice(index, index + 2) !== '*/') {
101
+ if (out[index] !== '\n')
102
+ out[index] = ' ';
103
+ index++;
104
+ }
105
+ if (index < out.length) {
106
+ out[index] = ' ';
107
+ out[index + 1] = ' ';
108
+ index += 2;
109
+ }
110
+ }
111
+ else {
112
+ index++;
113
+ }
114
+ }
115
+ return out.join('');
116
+ }
117
+ /** Verify the non-whitespace chars bracketing a quoted string are DOT punctuation, not stray text. */
118
+ // webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
119
+ function checkNeighbor(code, dot, start, end, source) {
120
+ const before = nonSpaceChar(code, start - 1, -1);
121
+ if (before !== undefined && !LEGAL_BEFORE_STRING.has(before)) {
122
+ throw new InvalidDotError(`${source}: a quoted string starts right after '${before}' at ${describe(dot, start)} — ` +
123
+ `an unescaped '"' in an interpolated value almost certainly ended the previous string early. ` +
124
+ `Interpolate values through dotValue().`);
125
+ }
126
+ const after = nonSpaceChar(code, end + 1, 1);
127
+ if (after !== undefined && !LEGAL_AFTER_STRING.has(after)) {
128
+ throw new InvalidDotError(`${source}: a quoted string is followed by '${after}' at ${describe(dot, end)} — ` +
129
+ `an unescaped '"' in an interpolated value almost certainly ended this string early. ` +
130
+ `Interpolate values through dotValue().`);
131
+ }
132
+ }
133
+ /** The first non-whitespace char walking `step` from `from`, or undefined at either end. */
134
+ // webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
135
+ function nonSpaceChar(dot, from, step) {
136
+ for (let i = from; i >= 0 && i < dot.length; i += step) {
137
+ if (!/\s/.test(dot[i]))
138
+ return dot[i];
139
+ }
140
+ return undefined;
141
+ }
142
+ /** `line N: <the line>` for the offset, so the error names the same line Graphviz would. */
143
+ // webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts
144
+ function describe(dot, offset) {
145
+ const lineNumber = dot.slice(0, offset).split('\n').length;
146
+ const line = dot.split('\n')[lineNumber - 1];
147
+ return `line ${lineNumber}: ${line.trim()}`;
148
+ }
149
+ //# sourceMappingURL=dot-syntax.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dot-syntax.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/dot-syntax.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * DOT syntax helpers\n *\n * Two small pieces that exist because a generated DOT that nobody parses is a DOT that WILL break:\n *\n * 1. `dotValue()` — the ONE place a runtime value (service name, api name, project name, title)\n * becomes safe to interpolate into a quoted DOT string. Inlining values at each call site is how\n * an unescaped `\"` shipped and took the whole diagram down: in DOT a bare `\"` TERMINATES the\n * string it appears in, so one bad node line makes the entire graph fail to parse.\n * 2. `assertValidDot()` — a structural check on the emitted DOT that turns exactly that class of\n * mistake into a thrown error at generation time, instead of a blank page with a Graphviz\n * \"syntax error in line N\" that only a human opening the HTML ever sees.\n */\n\n/** Chars a quoted string may legally sit directly after, ignoring whitespace. */\nconst LEGAL_BEFORE_STRING = new Set(['=', '[', ',', ';', '{', '}', '>', '-']);\n\n/** Chars a quoted string may legally be followed by, ignoring whitespace. */\nconst LEGAL_AFTER_STRING = new Set(['=', '[', ']', ',', ';', '{', '}', '-', '>']);\n\n/**\n * Escape a runtime value for use INSIDE a quoted DOT string.\n *\n * Only `\\` and `\"` matter: everything else (parens, spaces, `-`, `#`, unicode) is ordinary text once\n * it is inside quotes. Note this deliberately escapes `\\` FIRST, so a value containing a backslash\n * cannot smuggle an escape sequence in. Callers compose label lines with a literal `\\\\n` AFTER\n * escaping their values — the separator is ours, the value is theirs.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function dotValue(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"');\n}\n\n/** Thrown when the generator produces DOT that Graphviz could not parse. */\nexport class InvalidDotError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidDotError';\n }\n}\n\n/**\n * Fail loudly on structurally broken DOT.\n *\n * This is not a full Graphviz parser — it is the check that catches the failure mode a string\n * builder actually has: a quote that ends a string early (or never ends it). It scans the quoted\n * strings honouring `\\\"` escapes and asserts each one is terminated and is bounded by DOT\n * punctuation rather than by bare text. An unescaped `\"` inside a label always violates that: the\n * string ends mid-label, and the remaining label text becomes stray tokens.\n */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nexport function assertValidDot(dot: string, source: string): void {\n // Comment text is not code: a `\"` or a word in it must never be read as a DOT token. Blanking\n // it (offsets preserved) keeps every reported line number the one Graphviz would report.\n const code = blankComments(dot);\n let index = 0;\n while (index < code.length) {\n if (code[index] !== '\"') {\n index++;\n continue;\n }\n const start = index;\n index++;\n while (index < code.length && code[index] !== '\"') {\n index += code[index] === '\\\\' ? 2 : 1;\n }\n if (index >= code.length) {\n throw new InvalidDotError(`${source}: unterminated string starting at ${describe(dot, start)}`);\n }\n const end = index;\n index++;\n checkNeighbor(code, dot, start, end, source);\n }\n}\n\n/** Replace `//`, `#` and `/* *\\/` comment bodies with spaces, preserving length and newlines. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction blankComments(dot: string): string {\n const out = dot.split('');\n let index = 0;\n let inString = false;\n while (index < out.length) {\n const two = dot.slice(index, index + 2);\n if (inString) {\n if (dot[index] === '\\\\') index++;\n else if (dot[index] === '\"') inString = false;\n index++;\n } else if (dot[index] === '\"') {\n inString = true;\n index++;\n } else if (two === '//' || dot[index] === '#') {\n while (index < out.length && out[index] !== '\\n') out[index++] = ' ';\n } else if (two === '/*') {\n while (index < out.length && dot.slice(index, index + 2) !== '*/') {\n if (out[index] !== '\\n') out[index] = ' ';\n index++;\n }\n if (index < out.length) {\n out[index] = ' ';\n out[index + 1] = ' ';\n index += 2;\n }\n } else {\n index++;\n }\n }\n return out.join('');\n}\n\n/** Verify the non-whitespace chars bracketing a quoted string are DOT punctuation, not stray text. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction checkNeighbor(code: string, dot: string, start: number, end: number, source: string): void {\n const before = nonSpaceChar(code, start - 1, -1);\n if (before !== undefined && !LEGAL_BEFORE_STRING.has(before)) {\n throw new InvalidDotError(\n `${source}: a quoted string starts right after '${before}' at ${describe(dot, start)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended the previous string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n const after = nonSpaceChar(code, end + 1, 1);\n if (after !== undefined && !LEGAL_AFTER_STRING.has(after)) {\n throw new InvalidDotError(\n `${source}: a quoted string is followed by '${after}' at ${describe(dot, end)} — ` +\n `an unescaped '\"' in an interpolated value almost certainly ended this string early. ` +\n `Interpolate values through dotValue().`,\n );\n }\n}\n\n/** The first non-whitespace char walking `step` from `from`, or undefined at either end. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction nonSpaceChar(dot: string, from: number, step: number): string | undefined {\n for (let i = from; i >= 0 && i < dot.length; i += step) {\n if (!/\\s/.test(dot[i])) return dot[i];\n }\n return undefined;\n}\n\n/** `line N: <the line>` for the offset, so the error names the same line Graphviz would. */\n// webpieces-disable no-function-outside-class -- DOT string helpers, matching the sibling builders in runtime-visualizer.ts\nfunction describe(dot: string, offset: number): string {\n const lineNumber = dot.slice(0, offset).split('\\n').length;\n const line = dot.split('\\n')[lineNumber - 1];\n return `line ${lineNumber}: ${line.trim()}`;\n}\n"]}
@@ -24,6 +24,7 @@ exports.writeRuntimeVisualization = writeRuntimeVisualization;
24
24
  const tslib_1 = require("tslib");
25
25
  const fs = tslib_1.__importStar(require("fs"));
26
26
  const path = tslib_1.__importStar(require("path"));
27
+ const dot_syntax_1 = require("./dot-syntax");
27
28
  const LEVEL_COLORS = {
28
29
  0: '#E8F5E9',
29
30
  1: '#E3F2FD',
@@ -59,8 +60,9 @@ function getShortName(name) {
59
60
  // webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
60
61
  function labelList(entries) {
61
62
  const lines = [];
62
- for (let i = 0; i < entries.length; i += APIS_PER_LABEL_LINE) {
63
- lines.push(entries.slice(i, i + APIS_PER_LABEL_LINE).join(', '));
63
+ const safe = entries.map((entry) => (0, dot_syntax_1.dotValue)(entry));
64
+ for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {
65
+ lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));
64
66
  }
65
67
  return lines.join('\\n');
66
68
  }
@@ -83,8 +85,10 @@ function implementsEntries(svc) {
83
85
  // webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file
84
86
  function nodeLabel(name, svc) {
85
87
  const role = svc.implements.length > 0 ? 'server' : 'client';
86
- const declared = svc.serviceName === undefined ? '' : `, "${svc.serviceName}"`;
87
- let label = `${getShortName(name)}\\n(${role}, L${svc.level}${declared})`;
88
+ // The declared name is quoted for the reader those quotes MUST be DOT-escaped, or they end
89
+ // the label string and the whole graph stops parsing.
90
+ const declared = svc.serviceName === undefined ? '' : `, \\"${(0, dot_syntax_1.dotValue)(svc.serviceName)}\\"`;
91
+ let label = `${(0, dot_syntax_1.dotValue)(getShortName(name))}\\n(${role}, L${svc.level}${declared})`;
88
92
  if (svc.implements.length > 0)
89
93
  label += `\\nimplements: ${labelList(implementsEntries(svc))}`;
90
94
  if (svc.uses.length > 0)
@@ -98,9 +102,9 @@ function nodeLabel(name, svc) {
98
102
  */
99
103
  // webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file
100
104
  function edgeDot(edge) {
101
- const from = getShortName(edge.from);
102
- const to = getShortName(edge.to);
103
- const via = edge.via.map((v) => getShortName(v)).join(', ');
105
+ const from = (0, dot_syntax_1.dotValue)(getShortName(edge.from));
106
+ const to = (0, dot_syntax_1.dotValue)(getShortName(edge.to));
107
+ const via = edge.via.map((v) => (0, dot_syntax_1.dotValue)(getShortName(v))).join(', ');
104
108
  if (edge.type !== 'pubsub') {
105
109
  return ` "${from}" -> "${to}" [label="${via}"];\n`;
106
110
  }
@@ -125,7 +129,7 @@ function externalDot(graph, hidden) {
125
129
  for (const use of graph.unresolvedUses) {
126
130
  if (hidden.has(use.service))
127
131
  continue;
128
- const external = getShortName(graph.apis[use.api]?.owner ?? use.api);
132
+ const external = (0, dot_syntax_1.dotValue)(getShortName(graph.apis[use.api]?.owner ?? use.api));
129
133
  const key = `${use.service}${PAIR_SEP}${external}`;
130
134
  if (!apisByPair.has(key))
131
135
  apisByPair.set(key, []);
@@ -148,7 +152,7 @@ function externalDot(graph, hidden) {
148
152
  const external = parts[1];
149
153
  const via = labelList(apisByPair.get(key).sort());
150
154
  dot +=
151
- ` "${getShortName(service)}" -> "external__${external}" ` +
155
+ ` "${(0, dot_syntax_1.dotValue)(getShortName(service))}" -> "external__${external}" ` +
152
156
  `[label="${via}", style=dashed, color="${EXTERNAL_BORDER}"];\n`;
153
157
  }
154
158
  return dot;
@@ -168,7 +172,7 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
168
172
  continue;
169
173
  const svc = graph.services[name];
170
174
  const color = LEVEL_COLORS[svc.level] || '#F5F5F5';
171
- dot += ` "${getShortName(name)}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n`;
175
+ dot += ` "${(0, dot_syntax_1.dotValue)(getShortName(name))}" [fillcolor="${color}", label="${nodeLabel(name, svc)}"];\n`;
172
176
  }
173
177
  dot += '\n';
174
178
  for (const edge of graph.runtimeEdges) {
@@ -179,9 +183,12 @@ function generateRuntimeDot(graph, title = 'WebPieces Runtime Architecture', opt
179
183
  if (options.showExternalNodes)
180
184
  dot += externalDot(graph, hidden);
181
185
  dot += '\n labelloc="t";\n';
182
- dot += ` label="${title}\\n(from architecture/runtime-dependencies.json)";\n`;
186
+ dot += ` label="${(0, dot_syntax_1.dotValue)(title)}\\n(from architecture/runtime-dependencies.json)";\n`;
183
187
  dot += ' fontsize=20;\n';
184
188
  dot += '}\n';
189
+ // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked
190
+ // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.
191
+ (0, dot_syntax_1.assertValidDot)(dot, 'runtime-architecture.dot');
185
192
  return dot;
186
193
  }
187
194
  function generateRuntimeHtml(dot, title) {
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS and USES. That list is the\n * single most important fact in a microservice architecture, and it used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that\n * actually page you at 3am stop being missing from the picture. They are\n * RENDER-ONLY: derivation, levels and cycle detection never see them.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeService } from './runtime-graph';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n for (let i = 0; i < entries.length; i += APIS_PER_LABEL_LINE) {\n lines.push(entries.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it serves and\n * the contracts it calls. A node with neither reads exactly as before.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n const declared = svc.serviceName === undefined ? '' : `, \"${svc.serviceName}\"`;\n let label = `${getShortName(name)}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n if (svc.uses.length > 0) label += `\\\\nuses: ${labelList(svc.uses)}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the\n * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer\n * with a cylinder queue node and dashed enqueue/deliver arrows.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = getShortName(edge.from);\n const to = getShortName(edge.to);\n const via = edge.via.map((v: string) => getShortName(v)).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${via}\"];\\n`;\n }\n const queueId = `queue__${from}__${to}`;\n return (\n ` \"${queueId}\" [shape=cylinder, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", label=\"${via}\\\\nqueue\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n const external = getShortName(graph.apis[use.api]?.owner ?? use.api);\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n dot +=\n ` \"${getShortName(service)}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${getShortName(name)}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge);\n }\n\n if (options.showExternalNodes) dot += externalDot(graph, hidden);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${title}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n return dot;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n const script = `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(el => document.getElementById('graph').appendChild(el))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }\n .note { max-width: 700px; margin: 12px auto; color: #555; text-align: center; }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div class=\"note\">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>pubsub</strong> = producer &rarr; <em>queue</em> (cylinder) &rarr; consumer: the producer enqueues a Cloud Task and the consumer is delivered it later.</div>\n <div class=\"note\">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via &lt;lib&gt;)</em> means the service serves that contract through an embedded library rather than its own source. A <strong>dashed box</strong> is a system OUTSIDE this repo (firestore, gmail, ...): a contract this repo calls and nothing here implements.</div>\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}
1
+ {"version":3,"file":"runtime-visualizer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/runtime-visualizer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Runtime Visualizer\n *\n * Renders the runtime microservice graph (services + inferred Z -> X edges,\n * each labeled with the api(s) they flow over) to DOT + interactive HTML in\n * tmp/webpieces/runtime-architecture.{dot,html}.\n *\n * Each service node names the contracts it IMPLEMENTS and USES. That list is the\n * single most important fact in a microservice architecture, and it used to be\n * collapsed into a server/client boolean and thrown away — leaving an api that a\n * server serves but nothing in-repo calls completely invisible, and making a\n * correct api design look like a detection failure.\n *\n * Calls that leave the repo (a contract NOTHING in-repo implements — firestore,\n * gmail, ...) are drawn as dashed terminal nodes, so the vendor systems that\n * actually page you at 3am stop being missing from the picture. They are\n * RENDER-ONLY: derivation, levels and cycle detection never see them.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { RuntimeGraph, RuntimeEdge, RuntimeService } from './runtime-graph';\nimport { dotValue, assertValidDot } from './dot-syntax';\n\nconst LEVEL_COLORS: Record<number, string> = {\n 0: '#E8F5E9',\n 1: '#E3F2FD',\n 2: '#FFF3E0',\n 3: '#FCE4EC',\n};\n\nconst QUEUE_FILL = '#FFF3E0';\n\n/** Fill + border for the dashed terminal node standing for a system outside this repo. */\nconst EXTERNAL_FILL = '#FAFAFA';\nconst EXTERNAL_BORDER = '#9E9E9E';\n\n/** Apis per line inside a node label — beyond this the box grows wider than it is readable. */\nconst APIS_PER_LABEL_LINE = 3;\n\n/** Separator for the (service, external-library) grouping key; illegal in both project names. */\nconst PAIR_SEP = '|';\n\n/** Render options for the runtime graph. */\nexport class RuntimeVizOptions {\n constructor(\n /**\n * Draw the dashed terminal nodes for contracts nothing in-repo implements. On by default;\n * a repo whose external surface is noisy can turn them off in webpieces.config.json\n * (runtime-architecture.showExternalNodes).\n */\n public readonly showExternalNodes: boolean = true,\n ) {}\n}\n\nfunction getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n}\n\n/** Chunk a list into `\\n`-separated label lines of at most APIS_PER_LABEL_LINE entries. */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction labelList(entries: string[]): string {\n const lines: string[] = [];\n const safe = entries.map((entry: string) => dotValue(entry));\n for (let i = 0; i < safe.length; i += APIS_PER_LABEL_LINE) {\n lines.push(safe.slice(i, i + APIS_PER_LABEL_LINE).join(', '));\n }\n return lines.join('\\\\n');\n}\n\n/**\n * The implemented-api entries for a node label. An api served through an EMBEDDED LIBRARY is\n * annotated with that library, because \"who implements WarmupApi?\" otherwise requires knowing that\n * the derivation walks the dependsOn closure and then walking it by hand.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction implementsEntries(svc: RuntimeService): string[] {\n return svc.implements.map((api: string) => {\n const via = svc.implementsVia?.[api];\n return via === undefined ? api : `${api} (via ${getShortName(via)})`;\n });\n}\n\n/**\n * The full node label: name, role/level/declared service name, then the contracts it serves and\n * the contracts it calls. A node with neither reads exactly as before.\n */\n// webpieces-disable no-function-outside-class -- DOT label builder, matching getShortName in this file\nfunction nodeLabel(name: string, svc: RuntimeService): string {\n const role = svc.implements.length > 0 ? 'server' : 'client';\n // The declared name is quoted for the reader — those quotes MUST be DOT-escaped, or they end\n // the label string and the whole graph stops parsing.\n const declared = svc.serviceName === undefined ? '' : `, \\\\\"${dotValue(svc.serviceName)}\\\\\"`;\n let label = `${dotValue(getShortName(name))}\\\\n(${role}, L${svc.level}${declared})`;\n if (svc.implements.length > 0) label += `\\\\nimplements: ${labelList(implementsEntries(svc))}`;\n if (svc.uses.length > 0) label += `\\\\nuses: ${labelList(svc.uses)}`;\n return label;\n}\n\n/**\n * DOT for ONE runtime edge. rpc → a direct labeled arrow (producer calls consumer). pubsub → the\n * producer enqueues and the consumer is delivered later, so we draw producer → QUEUE → consumer\n * with a cylinder queue node and dashed enqueue/deliver arrows.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction edgeDot(edge: RuntimeEdge): string {\n const from = dotValue(getShortName(edge.from));\n const to = dotValue(getShortName(edge.to));\n const via = edge.via.map((v: string) => dotValue(getShortName(v))).join(', ');\n if (edge.type !== 'pubsub') {\n return ` \"${from}\" -> \"${to}\" [label=\"${via}\"];\\n`;\n }\n const queueId = `queue__${from}__${to}`;\n return (\n ` \"${queueId}\" [shape=cylinder, style=\"filled\", fillcolor=\"${QUEUE_FILL}\", label=\"${via}\\\\nqueue\"];\\n` +\n ` \"${from}\" -> \"${queueId}\" [label=\"enqueue\", style=dashed];\\n` +\n ` \"${queueId}\" -> \"${to}\" [label=\"deliver\", style=dashed];\\n`\n );\n}\n\n/**\n * The dashed terminal nodes + edges for calls that LEAVE the repo. Built from `unresolvedUses` —\n * a contract used by a node and implemented by nobody in-repo — which the derivation already\n * computes and which was, until now, only ever printed as a warning.\n *\n * Grouped by the api-lib that owns the contracts, so a service reaching three firestore contracts\n * draws ONE `lib-firestore (external)` box rather than three. These are drawn, never derived: they\n * are absent from levels, cycle detection and the transitive implements attribution.\n */\n// webpieces-disable no-function-outside-class -- DOT string builder, matching getShortName in this file\nfunction externalDot(graph: RuntimeGraph, hidden: Set<string>): string {\n // \"service|externalName\" -> the apis flowing over it.\n const apisByPair = new Map<string, string[]>();\n for (const use of graph.unresolvedUses) {\n if (hidden.has(use.service)) continue;\n const external = dotValue(getShortName(graph.apis[use.api]?.owner ?? use.api));\n const key = `${use.service}${PAIR_SEP}${external}`;\n if (!apisByPair.has(key)) apisByPair.set(key, []);\n apisByPair.get(key)!.push(use.api);\n }\n if (apisByPair.size === 0) return '';\n\n let dot = '\\n // Systems outside this repo — no in-repo service implements these contracts.\\n';\n // The node ID is prefixed so an external library can never collide with a service of the same\n // short name; only the label carries the bare name.\n const externals = new Set([...apisByPair.keys()].map((key: string) => key.split(PAIR_SEP)[1]));\n for (const external of [...externals].sort()) {\n dot +=\n ` \"external__${external}\" [shape=box, style=\"dashed,filled\", fillcolor=\"${EXTERNAL_FILL}\", ` +\n `color=\"${EXTERNAL_BORDER}\", label=\"${external}\\\\n(external)\"];\\n`;\n }\n for (const key of [...apisByPair.keys()].sort()) {\n const parts = key.split(PAIR_SEP);\n const service = parts[0];\n const external = parts[1];\n const via = labelList(apisByPair.get(key)!.sort());\n dot +=\n ` \"${dotValue(getShortName(service))}\" -> \"external__${external}\" ` +\n `[label=\"${via}\", style=dashed, color=\"${EXTERNAL_BORDER}\"];\\n`;\n }\n return dot;\n}\n\n/** Build the Graphviz DOT for the runtime service graph. */\n// webpieces-disable no-function-outside-class -- module entry point, matching the sibling builders here\nexport function generateRuntimeDot(\n graph: RuntimeGraph,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): string {\n let dot = 'digraph RuntimeArchitecture {\\n';\n dot += ' rankdir=TB;\\n';\n dot += ' node [shape=box, style=\"filled,rounded\", fontname=\"Arial\"];\\n';\n dot += ' edge [fontname=\"Arial\", fontsize=10];\\n\\n';\n\n // Services tagged drawOnGraph:false stay in the JSON but are omitted here —\n // both their node and any edge touching them are dropped from the render.\n const hidden = new Set(\n Object.keys(graph.services).filter((name: string) => graph.services[name].drawOnGraph === false)\n );\n\n for (const name of Object.keys(graph.services)) {\n if (hidden.has(name)) continue;\n const svc = graph.services[name];\n const color = LEVEL_COLORS[svc.level] || '#F5F5F5';\n dot += ` \"${dotValue(getShortName(name))}\" [fillcolor=\"${color}\", label=\"${nodeLabel(name, svc)}\"];\\n`;\n }\n\n dot += '\\n';\n\n for (const edge of graph.runtimeEdges) {\n if (hidden.has(edge.from) || hidden.has(edge.to)) continue;\n dot += edgeDot(edge);\n }\n\n if (options.showExternalNodes) dot += externalDot(graph, hidden);\n\n dot += '\\n labelloc=\"t\";\\n';\n dot += ` label=\"${dotValue(title)}\\\\n(from architecture/runtime-dependencies.json)\";\\n`;\n dot += ' fontsize=20;\\n';\n dot += '}\\n';\n // Nothing downstream parses this DOT until a human opens the page, so parse-shape is checked\n // HERE — a graph that cannot render is a generation failure, not a blank page to discover later.\n assertValidDot(dot, 'runtime-architecture.dot');\n return dot;\n}\n\nfunction generateRuntimeHtml(dot: string, title: string): string {\n const script = `\n const dot = ${JSON.stringify(dot)};\n const viz = new Viz();\n viz.renderSVGElement(dot)\n .then(el => document.getElementById('graph').appendChild(el))\n .catch(err => { document.getElementById('graph').innerHTML = '<pre>' + err + '</pre>'; });\n `;\n return `<!DOCTYPE html>\n<html>\n<head>\n <title>${title}</title>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/viz.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/viz.js@2.1.2/full.render.js\"></script>\n <style>\n body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }\n h1 { text-align: center; color: #333; }\n #graph { text-align: center; background: white; padding: 20px; border-radius: 8px; }\n .note { max-width: 700px; margin: 12px auto; color: #555; text-align: center; }\n </style>\n</head>\n<body>\n <h1>${title}</h1>\n <div class=\"note\">Runtime calls between services. <strong>rpc</strong> = a direct arrow (synchronous call, labeled with the api). <strong>pubsub</strong> = producer &rarr; <em>queue</em> (cylinder) &rarr; consumer: the producer enqueues a Cloud Task and the consumer is delivered it later.</div>\n <div class=\"note\">Each box lists the contracts it <strong>implements</strong> (serves) and <strong>uses</strong> (calls) — so an api a service serves is visible even when nothing in this repo calls it. <em>(via &lt;lib&gt;)</em> means the service serves that contract through an embedded library rather than its own source. A <strong>dashed box</strong> is a system OUTSIDE this repo (firestore, gmail, ...): a contract this repo calls and nothing here implements.</div>\n <div id=\"graph\"></div>\n <script>${script}</script>\n</body>\n</html>`;\n}\n\nexport interface RuntimeVisualizationPaths {\n dotPath: string;\n htmlPath: string;\n}\n\n/** Write the DOT + HTML renderings to tmp/webpieces/. */\nexport function writeRuntimeVisualization(\n graph: RuntimeGraph,\n workspaceRoot: string,\n title: string = 'WebPieces Runtime Architecture',\n options: RuntimeVizOptions = new RuntimeVizOptions(),\n): RuntimeVisualizationPaths {\n const outputDir = path.join(workspaceRoot, 'tmp', 'webpieces');\n if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n const dot = generateRuntimeDot(graph, title, options);\n const dotPath = path.join(outputDir, 'runtime-architecture.dot');\n fs.writeFileSync(dotPath, dot, 'utf-8');\n\n const htmlPath = path.join(outputDir, 'runtime-architecture.html');\n fs.writeFileSync(htmlPath, generateRuntimeHtml(dot, title), 'utf-8');\n\n return { dotPath, htmlPath };\n}\n"]}