@jpeakai/mermaidjs-lint 0.1.0 → 0.2.0
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/README.md +82 -53
- package/dist/bin.js +202 -44
- package/dist/index.js +206 -45
- package/dist/testing.js +86 -23
- package/examples/architecture_beta_iconify_logos.mmd +27 -0
- package/examples/flowchart_fontawesome_icons.mmd +31 -0
- package/examples/layout_cosebilkent_mindmap.mmd +19 -0
- package/examples/layout_dagre_classic.mmd +22 -0
- package/examples/layout_dagre_handdrawn.mmd +23 -0
- package/examples/layout_dagre_neo.mmd +26 -0
- package/examples/layout_elk_default.mmd +22 -0
- package/examples/layout_elk_tuned.mmd +25 -0
- package/examples/layout_tidytree_mindmap.mmd +19 -0
- package/package.json +2 -2
- package/src/check.ts +2 -3
- package/src/cli.ts +45 -23
- package/src/config.ts +3 -2
- package/src/errors.ts +18 -3
- package/src/files.ts +3 -1
- package/src/fix/engine.ts +4 -4
- package/src/fix/verify.ts +2 -2
- package/src/index.ts +1 -1
- package/src/model.ts +10 -0
- package/src/parse/color.ts +2 -2
- package/src/parse/contrast.ts +59 -5
- package/src/parse/metrics.ts +16 -8
- package/src/parse/stats.ts +45 -19
- package/src/plugins/contract.ts +1 -1
- package/src/plugins/discover.ts +5 -5
- package/src/plugins/load.ts +5 -3
- package/src/project.ts +11 -4
- package/src/rules/complexity-node-count-hard-limit.ts +1 -1
- package/src/rules/complexity-visual-score-critical.ts +2 -2
- package/src/rules/contrast-text-below-aa.ts +6 -2
- package/src/rules/index.ts +2 -1
- package/src/rules/parse-diagram-unparseable.ts +29 -14
- package/src/rules/registry.ts +7 -4
- package/src/rules/shared.ts +1 -1
- package/src/rules/types.ts +2 -2
- package/src/testing.ts +4 -3
package/dist/testing.js
CHANGED
|
@@ -183,17 +183,20 @@ function detectKeyword(content) {
|
|
|
183
183
|
}
|
|
184
184
|
async function extractStats(content) {
|
|
185
185
|
const keyword = detectKeyword(content) ?? "unknown";
|
|
186
|
-
|
|
186
|
+
const langium = keyword in LANGIUM_TYPES;
|
|
187
|
+
let langiumError = null;
|
|
188
|
+
if (langium) {
|
|
187
189
|
try {
|
|
188
190
|
return await extractLangiumStats(content, keyword);
|
|
189
|
-
} catch {
|
|
191
|
+
} catch (err) {
|
|
192
|
+
langiumError = parserReason(err);
|
|
193
|
+
}
|
|
190
194
|
}
|
|
191
|
-
let
|
|
195
|
+
let coreError;
|
|
192
196
|
try {
|
|
193
197
|
return await extractCoreStats(content, keyword);
|
|
194
198
|
} catch (err) {
|
|
195
|
-
|
|
196
|
-
`)[0] ?? null;
|
|
199
|
+
coreError = parserReason(err);
|
|
197
200
|
}
|
|
198
201
|
return {
|
|
199
202
|
nodes: 0,
|
|
@@ -202,11 +205,20 @@ async function extractStats(content) {
|
|
|
202
205
|
max_subgraph_depth: 0,
|
|
203
206
|
node_ids: [],
|
|
204
207
|
subgraph_names: [],
|
|
205
|
-
parser_used: "mermaid-core",
|
|
208
|
+
parser_used: langium ? "langium" : "mermaid-core",
|
|
206
209
|
diagram_type: keyword,
|
|
207
|
-
error
|
|
210
|
+
error: langiumError ?? coreError
|
|
208
211
|
};
|
|
209
212
|
}
|
|
213
|
+
var POINTER = /^-*\^$/;
|
|
214
|
+
var parserReason = (err) => {
|
|
215
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
216
|
+
const lines = message.split(`
|
|
217
|
+
`);
|
|
218
|
+
if (message.startsWith("No diagram type detected"))
|
|
219
|
+
return (lines[0] ?? "").trim();
|
|
220
|
+
return lines.filter((line, i) => !POINTER.test(line.trim()) && !POINTER.test((lines[i + 1] ?? "").trim())).join(" ").replace(/\s+/g, " ").trim();
|
|
221
|
+
};
|
|
210
222
|
async function langiumParseAny(type, content) {
|
|
211
223
|
switch (type) {
|
|
212
224
|
case "architecture":
|
|
@@ -692,6 +704,11 @@ var collectMermaid = (node, out) => {
|
|
|
692
704
|
for (const child of node.children ?? [])
|
|
693
705
|
collectMermaid(child, out);
|
|
694
706
|
};
|
|
707
|
+
var DIAGRAM_EXTS = [".mmd", ".mermaid"];
|
|
708
|
+
var documentKind = (file) => {
|
|
709
|
+
const lower = file.toLowerCase();
|
|
710
|
+
return DIAGRAM_EXTS.some((ext) => lower.endsWith(ext)) ? "diagram" : "markdown";
|
|
711
|
+
};
|
|
695
712
|
var buildDocModel = (src, kind = "markdown") => {
|
|
696
713
|
const lineStarts = lineStartsOf(src);
|
|
697
714
|
if (kind === "diagram") {
|
|
@@ -798,6 +815,14 @@ var MKDOCS_MATERIAL = {
|
|
|
798
815
|
};
|
|
799
816
|
var GITHUB = { light: { bg: "#ffffff" }, dark: { bg: "#0d1117" } };
|
|
800
817
|
var THEMES = ["light", "dark"];
|
|
818
|
+
var MERMAID_EDGE_LABEL = {
|
|
819
|
+
light: { box: "rgba(232,232,232,0.8)", backing: "rgba(232,232,232,0.5)" },
|
|
820
|
+
dark: { box: "#585858", backing: "rgba(88,88,88,0.5)" }
|
|
821
|
+
};
|
|
822
|
+
var edgeLabelBackground = (theme) => {
|
|
823
|
+
const { box, backing } = MERMAID_EDGE_LABEL[theme];
|
|
824
|
+
return compositeOver(box, compositeOver(box, compositeOver(backing, GITHUB[theme].bg)));
|
|
825
|
+
};
|
|
801
826
|
var MERMAID_ER = {
|
|
802
827
|
light: { odd: "#ffffff", even: "#f1f1ff", label: "#333333" },
|
|
803
828
|
dark: { odd: "#2c2d2d", even: "#060606", label: "#cccccc" }
|
|
@@ -818,10 +843,27 @@ var scorePair = (kind, d, fg, bg) => {
|
|
|
818
843
|
};
|
|
819
844
|
};
|
|
820
845
|
var message = (err) => err instanceof Error ? err.message : String(err);
|
|
846
|
+
var scoreEdgeLabel = (d, pairs, skipped) => {
|
|
847
|
+
const { color } = d.properties;
|
|
848
|
+
if (!color)
|
|
849
|
+
return;
|
|
850
|
+
try {
|
|
851
|
+
pairs.push(...THEMES.map((t) => scoreHostPair("text", d, t, color, edgeLabelBackground(t))));
|
|
852
|
+
} catch (err) {
|
|
853
|
+
skipped.push({
|
|
854
|
+
selector: d.selector,
|
|
855
|
+
line: d.line,
|
|
856
|
+
reason: `edge label text unparseable: ${message(err)}`,
|
|
857
|
+
blocking: true
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
};
|
|
821
861
|
var scoreGithub = (directives) => {
|
|
822
862
|
const pairs = [];
|
|
823
863
|
const skipped = [];
|
|
824
864
|
for (const d of directives) {
|
|
865
|
+
if (d.kind === "linkStyle")
|
|
866
|
+
scoreEdgeLabel(d, pairs, skipped);
|
|
825
867
|
if (d.kind !== "classDef" && d.kind !== "style")
|
|
826
868
|
continue;
|
|
827
869
|
const { fill, color, stroke } = d.properties;
|
|
@@ -886,6 +928,13 @@ var scoreMkdocs = (directives) => {
|
|
|
886
928
|
const pairs = [];
|
|
887
929
|
const skipped = [];
|
|
888
930
|
for (const d of directives) {
|
|
931
|
+
if (d.kind === "linkStyle" && d.properties.color) {
|
|
932
|
+
skipped.push({
|
|
933
|
+
selector: d.selector,
|
|
934
|
+
line: d.line,
|
|
935
|
+
reason: "color: ignored, because the host theme forces edge label text"
|
|
936
|
+
});
|
|
937
|
+
}
|
|
889
938
|
if (d.kind !== "classDef" && d.kind !== "style")
|
|
890
939
|
continue;
|
|
891
940
|
const { fill, stroke, color } = d.properties;
|
|
@@ -1108,15 +1157,17 @@ var calculateComplexity = (stats, config) => {
|
|
|
1108
1157
|
};
|
|
1109
1158
|
};
|
|
1110
1159
|
var parseFailed = (content, stats) => {
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
return t && !t.startsWith("%%") && t !== "---";
|
|
1115
|
-
});
|
|
1116
|
-
if (lines.length <= 1)
|
|
1160
|
+
if (stats.error !== null)
|
|
1161
|
+
return true;
|
|
1162
|
+
if (stats.parser_used === "langium" || stats.nodes > 0)
|
|
1117
1163
|
return false;
|
|
1118
|
-
return
|
|
1164
|
+
return sourceLines(content) > 1;
|
|
1119
1165
|
};
|
|
1166
|
+
var sourceLines = (content) => content.split(`
|
|
1167
|
+
`).filter((l) => {
|
|
1168
|
+
const t = l.trim();
|
|
1169
|
+
return t && !t.startsWith("%%") && t !== "---";
|
|
1170
|
+
}).length;
|
|
1120
1171
|
|
|
1121
1172
|
// src/rules/shared.ts
|
|
1122
1173
|
var COMPLEXITY_OPTIONS = {
|
|
@@ -1241,7 +1292,7 @@ var complexityNodeCountCognitiveLimit = complexityRule({
|
|
|
1241
1292
|
// src/rules/complexity-node-count-hard-limit.ts
|
|
1242
1293
|
var complexityNodeCountHardLimit = complexityRule({
|
|
1243
1294
|
id: RULE.NODES_HARD,
|
|
1244
|
-
summary: "more nodes than
|
|
1295
|
+
summary: "more nodes than the preset's hard limit",
|
|
1245
1296
|
hit: ({ stats }, c) => stats.nodes > c.node_hard_limit ? {
|
|
1246
1297
|
message: `${stats.nodes} nodes > ${c.node_hard_limit} hard limit`,
|
|
1247
1298
|
remediation: "Split immediately. This diagram is beyond any comprehensible size. " + `${boundaryHint(stats.subgraph_names).replace(/^along /, "Split along ")}. ${splitTarget(c)}`
|
|
@@ -1272,9 +1323,9 @@ var complexityVisualScoreAcceptableExceeded = complexityRule({
|
|
|
1272
1323
|
// src/rules/complexity-visual-score-critical.ts
|
|
1273
1324
|
var complexityVisualScoreCritical = complexityRule({
|
|
1274
1325
|
id: RULE.VCS_CRITICAL,
|
|
1275
|
-
summary: "a visual complexity score above the
|
|
1326
|
+
summary: "a visual complexity score above the preset's complex threshold",
|
|
1276
1327
|
hit: ({ stats, metrics }, c) => metrics.visual_complexity_score > c.vcs_complex ? {
|
|
1277
|
-
message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex}
|
|
1328
|
+
message: `VCS ${metrics.visual_complexity_score.toFixed(1)} > ${c.vcs_complex} complex threshold`,
|
|
1278
1329
|
remediation: `Reduce diagram complexity. Split ${boundaryHint(stats.subgraph_names)}, remove redundant edges, ` + `or collapse leaf nodes into labelled groups. ${splitTarget(c)}`
|
|
1279
1330
|
} : null
|
|
1280
1331
|
});
|
|
@@ -1360,7 +1411,7 @@ var contrastTextBelowAa = {
|
|
|
1360
1411
|
endLine: line,
|
|
1361
1412
|
rule: RULE.TEXT,
|
|
1362
1413
|
message: `${p.directive.kind} ${p.selector}: text ${p.assessment.ratio.toFixed(2)}:1 < ${AA_NORMAL2} ` + `(${p.foreground} on ${p.background}${where ? `, ${where}` : ""})`,
|
|
1363
|
-
remediation: profile === "mkdocs-material" ? "The host theme forces the label text, so change the fill until the host text reads on it in both themes." : "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme."
|
|
1414
|
+
remediation: profile === "mkdocs-material" ? "The host theme forces the label text, so change the fill until the host text reads on it in both themes." : p.directive.kind === "linkStyle" ? "An edge label sits on Mermaid's own label box, light in one theme and grey in the other, and no one colour reads on both, so drop color: from the linkStyle and let the theme set the label text." : "Darken or lighten the fill, or change color:, until the ratio reaches 4.5:1 in every theme."
|
|
1364
1415
|
});
|
|
1365
1416
|
}
|
|
1366
1417
|
}
|
|
@@ -1397,18 +1448,30 @@ var contrastTextBelowAa = {
|
|
|
1397
1448
|
};
|
|
1398
1449
|
|
|
1399
1450
|
// src/rules/parse-diagram-unparseable.ts
|
|
1451
|
+
var EXTERNAL_TYPES = new Set(["zenuml"]);
|
|
1452
|
+
var messageOf = (stats) => {
|
|
1453
|
+
const type = stats.diagram_type;
|
|
1454
|
+
if (EXTERNAL_TYPES.has(type)) {
|
|
1455
|
+
return `${type} is drawn by an external mermaid add-on that mermaidjs-lint does not include, so it cannot be parsed or checked`;
|
|
1456
|
+
}
|
|
1457
|
+
if (stats.error !== null) {
|
|
1458
|
+
const lines = /\bline \d/.test(stats.error) ? " (its line numbers count within the diagram, not the file)" : "";
|
|
1459
|
+
return `${type} did not parse (parser: ${stats.parser_used})${lines}: ${stats.error}`;
|
|
1460
|
+
}
|
|
1461
|
+
return `${type} parsed to 0 nodes from multi-line source with no error (parser: ${stats.parser_used}), a silent parse failure`;
|
|
1462
|
+
};
|
|
1400
1463
|
var parseDiagramUnparseable = {
|
|
1401
1464
|
id: RULE.UNPARSEABLE,
|
|
1402
1465
|
category: "parse",
|
|
1403
|
-
summary: "a
|
|
1466
|
+
summary: "a diagram mermaid's parser rejects, or reads as zero nodes from several lines",
|
|
1404
1467
|
check: async (ctx) => {
|
|
1405
1468
|
const found = [];
|
|
1406
1469
|
for (const diagram of ctx.doc.diagrams) {
|
|
1407
1470
|
const stats = await diagram.stats();
|
|
1408
1471
|
if (!parseFailed(diagram.source, stats))
|
|
1409
1472
|
continue;
|
|
1410
|
-
const
|
|
1411
|
-
found.push(finding(ctx, diagram, RULE.UNPARSEABLE,
|
|
1473
|
+
const remediation = EXTERNAL_TYPES.has(stats.diagram_type) ? `No fixer can help: mermaidjs-lint has no ${stats.diagram_type} parser. Redraw it as a type mermaid core includes, such as sequenceDiagram, or turn this rule off in the config.` : "Verify the diagram renders in mermaid.js and fix its syntax. Complexity rules are skipped for this diagram while it cannot be parsed.";
|
|
1474
|
+
found.push(finding(ctx, diagram, RULE.UNPARSEABLE, messageOf(stats), remediation));
|
|
1412
1475
|
}
|
|
1413
1476
|
return found;
|
|
1414
1477
|
}
|
|
@@ -1550,7 +1613,7 @@ var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
|
1550
1613
|
const findings = (await Promise.all(perRule)).flat();
|
|
1551
1614
|
return findings.sort((a, b) => a.line - b.line || a.rule.localeCompare(b.rule));
|
|
1552
1615
|
};
|
|
1553
|
-
var checkMarkdown = (src, file, flags = {}, registry = BUILTIN) => checkModel(buildDocModel(src, file
|
|
1616
|
+
var checkMarkdown = (src, file, flags = {}, registry = BUILTIN) => checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
|
|
1554
1617
|
|
|
1555
1618
|
// src/fix/verify.ts
|
|
1556
1619
|
var OMIT = new Set(["position", "data"]);
|
|
@@ -1619,7 +1682,7 @@ var overlaps = (edits) => {
|
|
|
1619
1682
|
};
|
|
1620
1683
|
var wellFormed = (e, length) => Number.isInteger(e.start) && Number.isInteger(e.end) && e.start >= 0 && e.start <= e.end && e.end <= length && typeof e.text === "string";
|
|
1621
1684
|
var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
1622
|
-
const kind = file
|
|
1685
|
+
const kind = documentKind(file);
|
|
1623
1686
|
const applied = [];
|
|
1624
1687
|
const refused = [];
|
|
1625
1688
|
const refusedKeys = new Set;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
architecture-beta
|
|
2
|
+
group ingress(logos:aws)[Ingress]
|
|
3
|
+
service dns(logos:aws-route53)[Route 53] in ingress
|
|
4
|
+
service cdn(logos:aws-cloudfront)[CloudFront] in ingress
|
|
5
|
+
|
|
6
|
+
group web(logos:aws-elb)[Web Tier]
|
|
7
|
+
service alb(logos:aws-elb)[Load Balancer] in web
|
|
8
|
+
service server(logos:aws-ec2)[Web Servers] in web
|
|
9
|
+
|
|
10
|
+
group app(logos:aws-ecs)[App Tier]
|
|
11
|
+
service worker(logos:aws-lambda)[Worker] in app
|
|
12
|
+
service api(logos:aws-ecs)[App Service] in app
|
|
13
|
+
|
|
14
|
+
group data(logos:aws-rds)[Data Tier]
|
|
15
|
+
service cache(logos:aws-elasticache)[Redis Cache] in data
|
|
16
|
+
service db(logos:aws-rds)[Primary DB] in data
|
|
17
|
+
service replica(logos:aws-rds)[Read Replica] in data
|
|
18
|
+
|
|
19
|
+
dns:R -[request]-> L:cdn
|
|
20
|
+
cdn:R -[route]-> L:alb
|
|
21
|
+
alb:R -[forward]-> L:server
|
|
22
|
+
server:R -[dispatch]-> L:worker
|
|
23
|
+
worker:R -[process]-> L:api
|
|
24
|
+
api:R -[read]-> L:cache
|
|
25
|
+
cache:R -[query]-> L:db
|
|
26
|
+
db:R -[replicate]-> L:replica
|
|
27
|
+
%% expect: none
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
flowchart LR
|
|
2
|
+
subgraph Ingress
|
|
3
|
+
dns["fa:fa-globe Route 53"]
|
|
4
|
+
cdn["fa:fa-cloud CloudFront"]
|
|
5
|
+
end
|
|
6
|
+
subgraph web["Web Tier"]
|
|
7
|
+
alb["fa:fa-random Load Balancer"]
|
|
8
|
+
web1["fa:fa-desktop Web 1"]
|
|
9
|
+
web2["fa:fa-desktop Web 2"]
|
|
10
|
+
end
|
|
11
|
+
subgraph app["App Tier"]
|
|
12
|
+
api["fa:fa-cog App Service"]
|
|
13
|
+
worker["fa:fa-bolt Worker"]
|
|
14
|
+
end
|
|
15
|
+
subgraph data["Data Tier"]
|
|
16
|
+
cache["fa:fa-bolt Redis Cache"]
|
|
17
|
+
db[("fa:fa-database Primary DB")]
|
|
18
|
+
replica[("fa:fa-copy Read Replica")]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
dns --> cdn
|
|
22
|
+
cdn --> alb
|
|
23
|
+
alb --> web1
|
|
24
|
+
alb --> web2
|
|
25
|
+
web1 --> api
|
|
26
|
+
web2 --> api
|
|
27
|
+
api --> cache
|
|
28
|
+
api --> db
|
|
29
|
+
worker --> db
|
|
30
|
+
db -- replication --> replica
|
|
31
|
+
%% expect: none
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
config:
|
|
3
|
+
layout: dagre
|
|
4
|
+
look: classic
|
|
5
|
+
theme: default
|
|
6
|
+
---
|
|
7
|
+
flowchart LR
|
|
8
|
+
user([User]) --> edge[CDN / Edge]
|
|
9
|
+
edge --> lb[Load Balancer]
|
|
10
|
+
lb --> api1[API Server 1]
|
|
11
|
+
lb --> api2[API Server 2]
|
|
12
|
+
api1 --> cache[(Cache)]
|
|
13
|
+
api2 --> cache
|
|
14
|
+
api1 --> db[(Primary DB)]
|
|
15
|
+
api2 --> db
|
|
16
|
+
api1 --> queue[[Job Queue]]
|
|
17
|
+
api2 --> queue
|
|
18
|
+
queue --> worker[Worker]
|
|
19
|
+
worker --> db
|
|
20
|
+
worker --> store[(Object Store)]
|
|
21
|
+
db -. replicate .-> replica[(Read Replica)]
|
|
22
|
+
%% expect: none
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
config:
|
|
3
|
+
layout: dagre
|
|
4
|
+
look: handDrawn
|
|
5
|
+
handDrawnSeed: 1
|
|
6
|
+
theme: default
|
|
7
|
+
---
|
|
8
|
+
flowchart LR
|
|
9
|
+
user([User]) --> edge[CDN / Edge]
|
|
10
|
+
edge --> lb[Load Balancer]
|
|
11
|
+
lb --> api1[API Server 1]
|
|
12
|
+
lb --> api2[API Server 2]
|
|
13
|
+
api1 --> cache[(Cache)]
|
|
14
|
+
api2 --> cache
|
|
15
|
+
api1 --> db[(Primary DB)]
|
|
16
|
+
api2 --> db
|
|
17
|
+
api1 --> queue[[Job Queue]]
|
|
18
|
+
api2 --> queue
|
|
19
|
+
queue --> worker[Worker]
|
|
20
|
+
worker --> db
|
|
21
|
+
worker --> store[(Object Store)]
|
|
22
|
+
db -. replicate .-> replica[(Read Replica)]
|
|
23
|
+
%% expect: none
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
config:
|
|
3
|
+
layout: dagre
|
|
4
|
+
look: neo
|
|
5
|
+
theme: default
|
|
6
|
+
useGradient: true
|
|
7
|
+
gradientStart: "#ede9fe"
|
|
8
|
+
gradientStop: "#a78bfa"
|
|
9
|
+
dropShadow: true
|
|
10
|
+
---
|
|
11
|
+
flowchart LR
|
|
12
|
+
user([User]) --> edge[CDN / Edge]
|
|
13
|
+
edge --> lb[Load Balancer]
|
|
14
|
+
lb --> api1[API Server 1]
|
|
15
|
+
lb --> api2[API Server 2]
|
|
16
|
+
api1 --> cache[(Cache)]
|
|
17
|
+
api2 --> cache
|
|
18
|
+
api1 --> db[(Primary DB)]
|
|
19
|
+
api2 --> db
|
|
20
|
+
api1 --> queue[[Job Queue]]
|
|
21
|
+
api2 --> queue
|
|
22
|
+
queue --> worker[Worker]
|
|
23
|
+
worker --> db
|
|
24
|
+
worker --> store[(Object Store)]
|
|
25
|
+
db -. replicate .-> replica[(Read Replica)]
|
|
26
|
+
%% expect: none
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
config:
|
|
3
|
+
layout: elk
|
|
4
|
+
look: classic
|
|
5
|
+
theme: default
|
|
6
|
+
---
|
|
7
|
+
flowchart LR
|
|
8
|
+
user([User]) --> edge[CDN / Edge]
|
|
9
|
+
edge --> lb[Load Balancer]
|
|
10
|
+
lb --> api1[API Server 1]
|
|
11
|
+
lb --> api2[API Server 2]
|
|
12
|
+
api1 --> cache[(Cache)]
|
|
13
|
+
api2 --> cache
|
|
14
|
+
api1 --> db[(Primary DB)]
|
|
15
|
+
api2 --> db
|
|
16
|
+
api1 --> queue[[Job Queue]]
|
|
17
|
+
api2 --> queue
|
|
18
|
+
queue --> worker[Worker]
|
|
19
|
+
worker --> db
|
|
20
|
+
worker --> store[(Object Store)]
|
|
21
|
+
db -. replicate .-> replica[(Read Replica)]
|
|
22
|
+
%% expect: none
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
config:
|
|
3
|
+
layout: elk
|
|
4
|
+
look: classic
|
|
5
|
+
theme: default
|
|
6
|
+
elk:
|
|
7
|
+
mergeEdges: true
|
|
8
|
+
nodePlacementStrategy: LINEAR_SEGMENTS
|
|
9
|
+
---
|
|
10
|
+
flowchart LR
|
|
11
|
+
user([User]) --> edge[CDN / Edge]
|
|
12
|
+
edge --> lb[Load Balancer]
|
|
13
|
+
lb --> api1[API Server 1]
|
|
14
|
+
lb --> api2[API Server 2]
|
|
15
|
+
api1 --> cache[(Cache)]
|
|
16
|
+
api2 --> cache
|
|
17
|
+
api1 --> db[(Primary DB)]
|
|
18
|
+
api2 --> db
|
|
19
|
+
api1 --> queue[[Job Queue]]
|
|
20
|
+
api2 --> queue
|
|
21
|
+
queue --> worker[Worker]
|
|
22
|
+
worker --> db
|
|
23
|
+
worker --> store[(Object Store)]
|
|
24
|
+
db -. replicate .-> replica[(Read Replica)]
|
|
25
|
+
%% expect: none
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jpeakai/mermaidjs-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"type": "git",
|
|
17
17
|
"url": "git+https://github.com/jpeakai/jpai-mermaidjs-lint.git"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.
|
|
19
|
+
"homepage": "https://jpeakai.github.io/jpai-mermaidjs-lint/",
|
|
20
20
|
"bugs": "https://github.com/jpeakai/jpai-mermaidjs-lint/issues",
|
|
21
21
|
"packageManager": "bun@1.3.12",
|
|
22
22
|
"publishConfig": {
|
package/src/check.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// them. A rule may be synchronous or return a promise, so a rule that waits on
|
|
5
5
|
// a tool or a service overlaps with the others.
|
|
6
6
|
|
|
7
|
-
import { buildDocModel, type DocModel } from "./model.ts";
|
|
7
|
+
import { buildDocModel, type DocModel, documentKind } from "./model.ts";
|
|
8
8
|
import { BUILTIN, HELPERS, type Registry } from "./rules/registry.ts";
|
|
9
9
|
import type { Finding, Rule, RuleOptions } from "./rules/types.ts";
|
|
10
10
|
|
|
@@ -44,5 +44,4 @@ export const checkMarkdown = (
|
|
|
44
44
|
file: string,
|
|
45
45
|
flags: RunFlags = {},
|
|
46
46
|
registry: Registry = BUILTIN,
|
|
47
|
-
): Promise<Finding[]> =>
|
|
48
|
-
checkModel(buildDocModel(src, file.toLowerCase().endsWith(".mmd") ? "diagram" : "markdown"), file, flags, registry);
|
|
47
|
+
): Promise<Finding[]> => checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
|
package/src/cli.ts
CHANGED
|
@@ -4,23 +4,29 @@
|
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
5
|
import { relative } from "node:path";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
|
+
import pkg from "../package.json" with { type: "json" };
|
|
7
8
|
import { checkModel, type RunFlags } from "./check.ts";
|
|
8
9
|
import { FlagError, MermaidLintError, UsageError } from "./errors.ts";
|
|
9
|
-
import { collectFiles } from "./files.ts";
|
|
10
|
+
import { collectFiles, DIAGRAM_FILE_EXTS, SKIP_DIRS } from "./files.ts";
|
|
10
11
|
import { fixSourceReport } from "./fix/engine.ts";
|
|
11
|
-
import { buildDocModel } from "./model.ts";
|
|
12
|
+
import { buildDocModel, DIAGRAM_EXTS, documentKind } from "./model.ts";
|
|
12
13
|
import { PROFILES } from "./parse/contrast.ts";
|
|
13
14
|
import { canonicalPreset, PRESET_NAMES } from "./parse/metrics.ts";
|
|
14
15
|
import type { PluginFailure } from "./plugins/index.ts";
|
|
15
16
|
import { setUp } from "./project.ts";
|
|
16
17
|
import { RULES } from "./rules/index.ts";
|
|
17
18
|
import type { LoadedSource, Registry } from "./rules/registry.ts";
|
|
18
|
-
import { CATEGORIES } from "./rules/types.ts";
|
|
19
|
+
import { CATEGORIES, type Finding } from "./rules/types.ts";
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
// The invocation forms, printed alone after a usage error so the error is not
|
|
22
|
+
// buried, and first in --help.
|
|
23
|
+
const SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME] [--profile NAME] [--config FILE]
|
|
21
24
|
[--no-plugins | --lenient-plugins]
|
|
22
25
|
mermaidjs-lint --list-rules
|
|
23
26
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
27
|
+
mermaidjs-lint --version | -h, --help`;
|
|
28
|
+
|
|
29
|
+
const USAGE = `${SYNOPSIS}
|
|
24
30
|
|
|
25
31
|
Deterministic Mermaid diagram gates (* = autofixable):
|
|
26
32
|
${CATEGORIES.map((c) =>
|
|
@@ -29,23 +35,29 @@ ${CATEGORIES.map((c) =>
|
|
|
29
35
|
),
|
|
30
36
|
).join("\n")}
|
|
31
37
|
|
|
32
|
-
A path is a
|
|
33
|
-
|
|
34
|
-
|
|
38
|
+
A path is a file or a directory, and every path must exist. A file ending in
|
|
39
|
+
${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
|
|
40
|
+
whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
|
|
41
|
+
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}.
|
|
42
|
+
Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
|
|
35
43
|
|
|
36
|
-
--preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
|
|
37
|
-
(aliases low, medium, high).
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
-p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
|
|
45
|
+
(aliases low, medium, high). It overrides a preset set in the config, and a
|
|
46
|
+
numeric threshold set in the config overrides both.
|
|
47
|
+
--profile NAME sets how contrast is read: ${PROFILES.join(", ")}. It overrides a profile set in the config.
|
|
48
|
+
auto reads mkdocs-material when any parent folder holds mkdocs.yml or mkdocs.yaml, else github.
|
|
40
49
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
41
50
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
42
51
|
--config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
|
|
43
52
|
Plugins load on their own from .mermaidjs-lint/rules and from dependencies named mermaidjs-lint-plugin-*.
|
|
44
|
-
--no-plugins runs the built-in rules only
|
|
53
|
+
--no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
|
|
54
|
+
and stops with a usage error rather than import a .mjs config, which is code.
|
|
45
55
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
46
56
|
--validate-plugins loads and checks every plugin, reports every failure, and runs no check or fix.
|
|
47
57
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
48
|
-
|
|
58
|
+
--version prints the version, to quote in a bug report.
|
|
59
|
+
Exits 0 when no findings remain, 1 when findings remain or a plugin fails to load (a lenient run
|
|
60
|
+
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
49
61
|
|
|
50
62
|
interface Parsed {
|
|
51
63
|
values: {
|
|
@@ -59,6 +71,7 @@ interface Parsed {
|
|
|
59
71
|
"lenient-plugins"?: boolean;
|
|
60
72
|
"validate-plugins"?: boolean;
|
|
61
73
|
"list-rules"?: boolean;
|
|
74
|
+
version?: boolean;
|
|
62
75
|
};
|
|
63
76
|
positionals: string[];
|
|
64
77
|
}
|
|
@@ -77,6 +90,7 @@ const parse = (argv: string[]): Parsed =>
|
|
|
77
90
|
"lenient-plugins": { type: "boolean", default: false },
|
|
78
91
|
"validate-plugins": { type: "boolean", default: false },
|
|
79
92
|
"list-rules": { type: "boolean", default: false },
|
|
93
|
+
version: { type: "boolean", default: false },
|
|
80
94
|
},
|
|
81
95
|
allowPositionals: true,
|
|
82
96
|
strict: true,
|
|
@@ -154,6 +168,13 @@ const validate = (
|
|
|
154
168
|
return { code, stdout: line, stderr: [] };
|
|
155
169
|
};
|
|
156
170
|
|
|
171
|
+
// One finding as the text report prints it: file, line or line range, rule
|
|
172
|
+
// and message. The site generator prints its examples with this too.
|
|
173
|
+
export const reportLine = (f: Finding, file: string): string => {
|
|
174
|
+
const where = f.endLine > f.line ? `${f.line}-${f.endLine}` : `${f.line}`;
|
|
175
|
+
return `${file}:${where} ${f.rule} ${f.message}`;
|
|
176
|
+
};
|
|
177
|
+
|
|
157
178
|
const display = (file: string, cwd: string): string => {
|
|
158
179
|
const rel = relative(cwd, file);
|
|
159
180
|
return !rel || rel.startsWith("..") ? file : rel;
|
|
@@ -171,6 +192,10 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
171
192
|
console.log(USAGE);
|
|
172
193
|
return 0;
|
|
173
194
|
}
|
|
195
|
+
if (values.version) {
|
|
196
|
+
console.log(pkg.version);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
174
199
|
const listing = values["list-rules"];
|
|
175
200
|
const validating = values["validate-plugins"];
|
|
176
201
|
if (values.preset !== undefined && canonicalPreset(values.preset) === null) {
|
|
@@ -228,9 +253,7 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
228
253
|
src = fixed;
|
|
229
254
|
}
|
|
230
255
|
}
|
|
231
|
-
|
|
232
|
-
const found = await checkModel(buildDocModel(src, kind), display(file, cwd), flags, registry);
|
|
233
|
-
return found;
|
|
256
|
+
return checkModel(buildDocModel(src, documentKind(file)), display(file, cwd), flags, registry);
|
|
234
257
|
}),
|
|
235
258
|
);
|
|
236
259
|
const all = perFile.flat();
|
|
@@ -246,10 +269,7 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
246
269
|
JSON.stringify({ findings: all, files: files.length, plugins: registry.sources, skippedPlugins }, null, 2),
|
|
247
270
|
);
|
|
248
271
|
} else {
|
|
249
|
-
for (const f of all)
|
|
250
|
-
const where = f.endLine > f.line ? `${f.line}-${f.endLine}` : `${f.line}`;
|
|
251
|
-
console.log(`${f.file}:${where} ${f.rule} ${f.message}`);
|
|
252
|
-
}
|
|
272
|
+
for (const f of all) console.log(reportLine(f, f.file));
|
|
253
273
|
console.log(`${all.length} finding(s) in ${files.length} file(s)`);
|
|
254
274
|
}
|
|
255
275
|
return all.length > 0 ? 1 : 0;
|
|
@@ -261,15 +281,17 @@ export const describeError = (err: unknown): string =>
|
|
|
261
281
|
? `error[${err.code}]: ${err.message}`
|
|
262
282
|
: `error: ${err instanceof Error ? err.message : String(err)}`;
|
|
263
283
|
|
|
264
|
-
// A usage error prints its reason
|
|
265
|
-
// a runtime failure and propagates to
|
|
284
|
+
// A usage error prints its reason, the invocation forms and a pointer to
|
|
285
|
+
// --help, and exits 2. Anything else is a runtime failure and propagates to
|
|
286
|
+
// bin.ts, which exits 1.
|
|
266
287
|
export const main = async (argv: string[] = process.argv.slice(2), cwd: string = process.cwd()): Promise<number> => {
|
|
267
288
|
try {
|
|
268
289
|
return await run(argv, cwd);
|
|
269
290
|
} catch (err) {
|
|
270
291
|
if (!(err instanceof UsageError)) throw err;
|
|
271
292
|
console.error(describeError(err));
|
|
272
|
-
console.error(
|
|
293
|
+
console.error(SYNOPSIS);
|
|
294
|
+
console.error("run mermaidjs-lint --help for more");
|
|
273
295
|
return 2;
|
|
274
296
|
}
|
|
275
297
|
};
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The config file: which rules are off, what options each takes, and how
|
|
2
2
|
// strictly plugins load. It is optional, because everything auto-detected loads
|
|
3
3
|
// without it. A mistake in it is a named usage error, so a stale or misspelt
|
|
4
|
-
// rule id fails the run instead of silently doing nothing.
|
|
4
|
+
// built-in rule id fails the run instead of silently doing nothing. A lenient
|
|
5
|
+
// or collect (validating) run drops a setting for an unloaded plugin rule.
|
|
5
6
|
|
|
6
7
|
import { access, readFile } from "node:fs/promises";
|
|
7
8
|
import { extname, join } from "node:path";
|
|
@@ -76,7 +77,7 @@ export const parseConfig = (file: string, raw: unknown): Config => {
|
|
|
76
77
|
};
|
|
77
78
|
};
|
|
78
79
|
|
|
79
|
-
// The
|
|
80
|
+
// The config file at the root, or null. Two at once is ambiguous, so it
|
|
80
81
|
// is an error rather than a pick.
|
|
81
82
|
export const findConfig = async (root: string): Promise<string | null> => {
|
|
82
83
|
const found: string[] = [];
|