@jpeakai/mermaidjs-lint 0.2.0 → 0.4.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 +12 -9
- package/dist/bin.js +214 -64
- package/dist/index.js +217 -64
- package/dist/testing.js +99 -32
- package/dist/types/bin.d.ts +2 -0
- package/dist/types/check.d.ts +10 -0
- package/dist/types/cli.d.ts +9 -0
- package/dist/types/config.d.ts +20 -0
- package/dist/types/errors.d.ts +88 -0
- package/dist/types/files.d.ts +8 -0
- package/dist/types/fix/engine.d.ts +10 -0
- package/dist/types/fix/verify.d.ts +12 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/model.d.ts +27 -0
- package/dist/types/parse/color.d.ts +28 -0
- package/dist/types/parse/contrast.d.ts +35 -0
- package/dist/types/parse/metrics.d.ts +48 -0
- package/dist/types/parse/stats.d.ts +25 -0
- package/dist/types/plugins/contract.d.ts +24 -0
- package/dist/types/plugins/discover.d.ts +20 -0
- package/dist/types/plugins/index.d.ts +19 -0
- package/dist/types/plugins/load.d.ts +14 -0
- package/dist/types/plugins/runtime.d.ts +6 -0
- package/dist/types/project.d.ts +18 -0
- package/dist/types/rules/complexity-node-count-acceptable-exceeded.d.ts +1 -0
- package/dist/types/rules/complexity-node-count-cognitive-limit.d.ts +1 -0
- package/dist/types/rules/complexity-node-count-hard-limit.d.ts +1 -0
- package/dist/types/rules/complexity-subgraph-nesting-too-deep.d.ts +2 -0
- package/dist/types/rules/complexity-visual-score-acceptable-exceeded.d.ts +1 -0
- package/dist/types/rules/complexity-visual-score-critical.d.ts +1 -0
- package/dist/types/rules/complexity.d.ts +12 -0
- package/dist/types/rules/contrast-border-below-ui-minimum.d.ts +2 -0
- package/dist/types/rules/contrast-directive-unauditable.d.ts +2 -0
- package/dist/types/rules/contrast-text-below-aa.d.ts +2 -0
- package/dist/types/rules/index.d.ts +3 -0
- package/dist/types/rules/parse-diagram-unparseable.d.ts +2 -0
- package/dist/types/rules/parse-fence-unterminated.d.ts +2 -0
- package/dist/types/rules/registry.d.ts +29 -0
- package/dist/types/rules/shared.d.ts +18 -0
- package/dist/types/rules/types.d.ts +67 -0
- package/dist/types/testing.d.ts +15 -0
- package/package.json +5 -4
- package/src/cli.ts +47 -15
- package/src/errors.ts +46 -0
- package/src/files.ts +21 -2
- package/src/fix/engine.ts +22 -10
- package/src/fix/verify.ts +31 -3
- package/src/index.ts +5 -3
- package/src/parse/metrics.ts +8 -1
- package/src/parse/stats.ts +72 -16
- package/src/plugins/contract.ts +4 -2
- package/src/plugins/discover.ts +38 -15
- package/src/plugins/index.ts +5 -3
- package/src/plugins/load.ts +32 -1
- package/src/project.ts +3 -1
- package/src/rules/complexity-subgraph-nesting-too-deep.ts +5 -0
- package/src/rules/parse-fence-unterminated.ts +1 -1
package/dist/bin.js
CHANGED
|
@@ -3,13 +3,13 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/cli.ts
|
|
6
|
-
import { readFile as readFile3, writeFile } from "node:fs/promises";
|
|
7
|
-
import { relative } from "node:path";
|
|
6
|
+
import { lstat, readFile as readFile3, realpath, writeFile } from "node:fs/promises";
|
|
7
|
+
import { isAbsolute as isAbsolute2, relative, resolve as resolve5 } from "node:path";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
// package.json
|
|
10
10
|
var package_default = {
|
|
11
11
|
name: "@jpeakai/mermaidjs-lint",
|
|
12
|
-
version: "0.
|
|
12
|
+
version: "0.4.0",
|
|
13
13
|
type: "module",
|
|
14
14
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
15
15
|
license: "MIT",
|
|
@@ -38,15 +38,16 @@ var package_default = {
|
|
|
38
38
|
bin: {
|
|
39
39
|
"mermaidjs-lint": "dist/bin.js"
|
|
40
40
|
},
|
|
41
|
+
types: "./dist/types/index.d.ts",
|
|
41
42
|
exports: {
|
|
42
43
|
".": {
|
|
44
|
+
types: "./dist/types/index.d.ts",
|
|
43
45
|
bun: "./src/index.ts",
|
|
44
|
-
types: "./src/index.ts",
|
|
45
46
|
default: "./dist/index.js"
|
|
46
47
|
},
|
|
47
48
|
"./testing": {
|
|
49
|
+
types: "./dist/types/testing.d.ts",
|
|
48
50
|
bun: "./src/testing.ts",
|
|
49
|
-
types: "./src/testing.ts",
|
|
50
51
|
default: "./dist/testing.js"
|
|
51
52
|
}
|
|
52
53
|
},
|
|
@@ -60,6 +61,7 @@ var package_default = {
|
|
|
60
61
|
],
|
|
61
62
|
dependencies: {
|
|
62
63
|
"@mermaid-js/parser": "^1.1.0",
|
|
64
|
+
"@types/mdast": "^4",
|
|
63
65
|
"colorjs.io": "^0.6.1",
|
|
64
66
|
"happy-dom": "^20.9.0",
|
|
65
67
|
"mdast-util-from-markdown": "^2.0.3",
|
|
@@ -73,7 +75,6 @@ var package_default = {
|
|
|
73
75
|
"@biomejs/biome": "^2",
|
|
74
76
|
"@jpeakai/prose-gates": "^0.2.0",
|
|
75
77
|
"@types/bun": "latest",
|
|
76
|
-
"@types/mdast": "^4",
|
|
77
78
|
typescript: "^5"
|
|
78
79
|
}
|
|
79
80
|
};
|
|
@@ -280,6 +281,7 @@ async function extractStats(content) {
|
|
|
280
281
|
edges: 0,
|
|
281
282
|
subgraphs: 0,
|
|
282
283
|
max_subgraph_depth: 0,
|
|
284
|
+
max_tree_depth: 0,
|
|
283
285
|
node_ids: [],
|
|
284
286
|
subgraph_names: [],
|
|
285
287
|
parser_used: langium ? "langium" : "mermaid-core",
|
|
@@ -329,6 +331,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
329
331
|
const subgraphNames = [];
|
|
330
332
|
let edges = 0;
|
|
331
333
|
let maxDepth = 0;
|
|
334
|
+
let treeDepth = 0;
|
|
332
335
|
switch (mapped) {
|
|
333
336
|
case "architecture": {
|
|
334
337
|
const services = ast.services ?? [];
|
|
@@ -375,7 +378,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
375
378
|
const stats = indentTreeStats(nodes.map((n, i) => ({ label: n.name ?? `node_${i}`, indent: n.indent ?? 0 })));
|
|
376
379
|
nodeIds.push(...stats.labels);
|
|
377
380
|
edges = stats.edges;
|
|
378
|
-
|
|
381
|
+
treeDepth = stats.depth;
|
|
379
382
|
break;
|
|
380
383
|
}
|
|
381
384
|
case "treemap": {
|
|
@@ -383,7 +386,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
383
386
|
const stats = indentTreeStats(rows.map((r, i) => ({ label: r.item?.name ?? `row_${i}`, indent: r.indent ?? 0 })));
|
|
384
387
|
nodeIds.push(...stats.labels);
|
|
385
388
|
edges = stats.edges;
|
|
386
|
-
|
|
389
|
+
treeDepth = stats.depth;
|
|
387
390
|
break;
|
|
388
391
|
}
|
|
389
392
|
case "wardley": {
|
|
@@ -403,6 +406,7 @@ async function extractLangiumStats(content, keyword) {
|
|
|
403
406
|
edges,
|
|
404
407
|
subgraphs: subgraphNames.length,
|
|
405
408
|
max_subgraph_depth: maxDepth,
|
|
409
|
+
max_tree_depth: treeDepth,
|
|
406
410
|
node_ids: [...new Set(nodeIds.filter(Boolean))].sort(),
|
|
407
411
|
subgraph_names: subgraphNames,
|
|
408
412
|
parser_used: "langium",
|
|
@@ -453,12 +457,13 @@ async function extractCoreStats(content, keyword) {
|
|
|
453
457
|
if (!adapter) {
|
|
454
458
|
throw new Error(`no core adapter for diagram.type="${diagramType}" (keyword="${keyword}")`);
|
|
455
459
|
}
|
|
456
|
-
const { nodeIds, edges, subgraphNames, maxDepth } = adapter(diagram.db);
|
|
460
|
+
const { nodeIds, edges, subgraphNames, maxDepth, treeDepth = 0 } = adapter(diagram.db);
|
|
457
461
|
return {
|
|
458
462
|
nodes: nodeIds.length,
|
|
459
463
|
edges,
|
|
460
464
|
subgraphs: subgraphNames.length,
|
|
461
465
|
max_subgraph_depth: maxDepth,
|
|
466
|
+
max_tree_depth: treeDepth,
|
|
462
467
|
node_ids: [...nodeIds].sort(),
|
|
463
468
|
subgraph_names: subgraphNames,
|
|
464
469
|
parser_used: "mermaid-core",
|
|
@@ -469,6 +474,7 @@ async function extractCoreStats(content, keyword) {
|
|
|
469
474
|
var CORE_ADAPTERS = {
|
|
470
475
|
"flowchart-v2": adaptFlowchart,
|
|
471
476
|
flowchart: adaptFlowchart,
|
|
477
|
+
"flowchart-elk": adaptFlowchart,
|
|
472
478
|
graph: adaptFlowchart,
|
|
473
479
|
classDiagram: adaptClass,
|
|
474
480
|
"classDiagram-v2": adaptClass,
|
|
@@ -592,7 +598,13 @@ function adaptIshikawa(db) {
|
|
|
592
598
|
walk(c, depth + 1);
|
|
593
599
|
};
|
|
594
600
|
walk(root, 0);
|
|
595
|
-
return {
|
|
601
|
+
return {
|
|
602
|
+
nodeIds: labels,
|
|
603
|
+
edges: Math.max(0, labels.length - 1),
|
|
604
|
+
subgraphNames: [],
|
|
605
|
+
maxDepth: 0,
|
|
606
|
+
treeDepth: maxDepth
|
|
607
|
+
};
|
|
596
608
|
}
|
|
597
609
|
function adaptKanban(db) {
|
|
598
610
|
const data = callGetter(db, "getData");
|
|
@@ -620,16 +632,20 @@ function adaptMindmap(db) {
|
|
|
620
632
|
walk(c, depth + 1);
|
|
621
633
|
};
|
|
622
634
|
walk(root, 0);
|
|
623
|
-
return {
|
|
635
|
+
return {
|
|
636
|
+
nodeIds: labels,
|
|
637
|
+
edges: Math.max(0, labels.length - 1),
|
|
638
|
+
subgraphNames: [],
|
|
639
|
+
maxDepth: 0,
|
|
640
|
+
treeDepth: maxDepth
|
|
641
|
+
};
|
|
624
642
|
}
|
|
625
643
|
function adaptQuadrantChart(db) {
|
|
626
644
|
const data = callGetter(db, "getQuadrantData");
|
|
627
|
-
const
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
});
|
|
632
|
-
return { nodeIds: labels, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
645
|
+
const textOf = (t) => typeof t === "string" ? t : t?.text ?? "";
|
|
646
|
+
const quadrants = (data?.quadrants ?? []).map((q) => textOf(q.text)).filter((t) => t.trim() !== "");
|
|
647
|
+
const points = (data?.points ?? []).map((p, i) => textOf(p.text) || `point_${i}`);
|
|
648
|
+
return { nodeIds: [...quadrants, ...points], edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
633
649
|
}
|
|
634
650
|
function adaptRequirement(db) {
|
|
635
651
|
const requirements = db.requirements instanceof Map ? db.requirements : new Map;
|
|
@@ -655,18 +671,30 @@ function adaptSankey(db) {
|
|
|
655
671
|
function adaptState(db) {
|
|
656
672
|
const nodes = db.nodes ?? [];
|
|
657
673
|
const edges = db.edges ?? [];
|
|
674
|
+
const groups = nodes.filter((n) => n.isGroup === true);
|
|
675
|
+
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
676
|
+
let maxDepth = 0;
|
|
677
|
+
for (const g of groups) {
|
|
678
|
+
let depth = 1;
|
|
679
|
+
const seen = new Set;
|
|
680
|
+
for (let cur = g.parentId;cur && !seen.has(cur); cur = parentOf.get(cur)) {
|
|
681
|
+
seen.add(cur);
|
|
682
|
+
depth++;
|
|
683
|
+
}
|
|
684
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
685
|
+
}
|
|
658
686
|
return {
|
|
659
687
|
nodeIds: nodes.map((n) => n.id ?? ""),
|
|
660
688
|
edges: edges.length,
|
|
661
|
-
subgraphNames:
|
|
662
|
-
maxDepth
|
|
689
|
+
subgraphNames: groups.map((g) => g.id ?? ""),
|
|
690
|
+
maxDepth
|
|
663
691
|
};
|
|
664
692
|
}
|
|
665
693
|
function adaptTimeline(db) {
|
|
666
694
|
const tasks = callGetter(db, "getTasks") ?? [];
|
|
667
695
|
const sections = callGetter(db, "getSections") ?? [];
|
|
668
696
|
return {
|
|
669
|
-
nodeIds: tasks.
|
|
697
|
+
nodeIds: tasks.flatMap((t) => [t.task ?? "", ...(t.events ?? []).map((e) => `${t.task ?? ""}: ${e}`)]),
|
|
670
698
|
edges: 0,
|
|
671
699
|
subgraphNames: sections.map((s) => s.sectionName ?? s.section ?? ""),
|
|
672
700
|
maxDepth: sections.length > 0 ? 1 : 0
|
|
@@ -696,7 +724,10 @@ function adaptVenn(db) {
|
|
|
696
724
|
function adaptXychart(db) {
|
|
697
725
|
const data = callGetter(db, "getXYChartData");
|
|
698
726
|
const categories = data?.xAxis?.categories ?? [];
|
|
699
|
-
|
|
727
|
+
if (categories.length > 0)
|
|
728
|
+
return { nodeIds: categories, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
729
|
+
const longest = Math.max(0, ...(data?.plots ?? []).map((p) => p.data?.length ?? 0));
|
|
730
|
+
return { nodeIds: Array.from({ length: longest }, (_, i) => `x_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
700
731
|
}
|
|
701
732
|
function subgraphNestingDepth(subGraphs) {
|
|
702
733
|
if (subGraphs.length === 0)
|
|
@@ -923,6 +954,34 @@ class RuleOptionError extends UsageError {
|
|
|
923
954
|
}
|
|
924
955
|
}
|
|
925
956
|
|
|
957
|
+
class FixOutsideRootError extends UsageError {
|
|
958
|
+
file;
|
|
959
|
+
target;
|
|
960
|
+
constructor(file, target, root) {
|
|
961
|
+
super(`${file} is a symbolic link to ${target}, outside the project root ${root}; --fix does not write through it. Fix the target in its own project, or run on the target path itself`, "fix-outside-root");
|
|
962
|
+
this.file = file;
|
|
963
|
+
this.target = target;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
class PackageJsonError extends MermaidLintError {
|
|
968
|
+
file;
|
|
969
|
+
constructor(file) {
|
|
970
|
+
super("package-json", `${file} is not a JSON object; fix or remove it`);
|
|
971
|
+
this.file = file;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
class FixNotConvergingError extends MermaidLintError {
|
|
976
|
+
file;
|
|
977
|
+
rules;
|
|
978
|
+
constructor(file, passes, rules) {
|
|
979
|
+
super("fix-not-converging", `${file}: --fix did not converge after ${passes} passes; ${rules.join(", ")} kept proposing edits. A fixer must propose nothing once its edit is applied`);
|
|
980
|
+
this.file = file;
|
|
981
|
+
this.rules = rules;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
926
985
|
class PluginLoadError extends MermaidLintError {
|
|
927
986
|
source;
|
|
928
987
|
constructor(source, reason, code = "plugin") {
|
|
@@ -1314,7 +1373,7 @@ var calculateComplexity = (stats, config) => {
|
|
|
1314
1373
|
const n = stats.nodes;
|
|
1315
1374
|
const e = stats.edges;
|
|
1316
1375
|
const s = stats.subgraphs;
|
|
1317
|
-
const d = stats.max_subgraph_depth;
|
|
1376
|
+
const d = Math.max(stats.max_subgraph_depth, stats.max_tree_depth);
|
|
1318
1377
|
const base = n + e * config.edge_weight + s * config.subgraph_weight;
|
|
1319
1378
|
const vcs = base * (1 + d * config.depth_weight);
|
|
1320
1379
|
const maxEdges = n > 1 ? n * (n - 1) : 0;
|
|
@@ -1652,7 +1711,7 @@ var parseFenceUnterminated = {
|
|
|
1652
1711
|
id: RULE.FENCE_UNTERMINATED,
|
|
1653
1712
|
category: "parse",
|
|
1654
1713
|
summary: "a mermaid fence that is never closed",
|
|
1655
|
-
check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, `mermaid fence opened at line ${d.startLine} is never closed`, "Add a closing fence after the last line of the diagram.")),
|
|
1714
|
+
check: (ctx) => ctx.doc.diagrams.filter((d) => !d.terminated).map((d) => finding(ctx, d, RULE.FENCE_UNTERMINATED, `mermaid fence opened at line ${d.startLine} is never closed`, "Add a closing fence directly after the last line of the diagram, with the opening line's prefix: `> ` in a quote, the item's indent in a list.")),
|
|
1656
1715
|
fix: (ctx) => {
|
|
1657
1716
|
const edits = [];
|
|
1658
1717
|
const { src } = ctx.doc;
|
|
@@ -1789,9 +1848,17 @@ import { extname, join as join2, resolve as resolve2 } from "node:path";
|
|
|
1789
1848
|
var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
|
|
1790
1849
|
var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
|
|
1791
1850
|
var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
|
|
1851
|
+
var linksToDir = (path) => {
|
|
1852
|
+
try {
|
|
1853
|
+
return statSync(path).isDirectory();
|
|
1854
|
+
} catch {
|
|
1855
|
+
return false;
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1792
1858
|
var collectFiles = (paths) => {
|
|
1793
1859
|
const files = [];
|
|
1794
1860
|
const unreadable = [];
|
|
1861
|
+
const symlinks = [];
|
|
1795
1862
|
const seen = new Set;
|
|
1796
1863
|
const walk = (dir) => {
|
|
1797
1864
|
let entries;
|
|
@@ -1803,7 +1870,11 @@ var collectFiles = (paths) => {
|
|
|
1803
1870
|
}
|
|
1804
1871
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1805
1872
|
const abs = join2(dir, entry.name);
|
|
1806
|
-
if (entry.
|
|
1873
|
+
if (entry.isSymbolicLink()) {
|
|
1874
|
+
if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
|
|
1875
|
+
symlinks.push(abs);
|
|
1876
|
+
}
|
|
1877
|
+
} else if (entry.isDirectory()) {
|
|
1807
1878
|
if (!skipDir(entry.name))
|
|
1808
1879
|
walk(abs);
|
|
1809
1880
|
} else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
|
|
@@ -1828,7 +1899,7 @@ var collectFiles = (paths) => {
|
|
|
1828
1899
|
unreadable.push(p);
|
|
1829
1900
|
}
|
|
1830
1901
|
}
|
|
1831
|
-
return { files, unreadable };
|
|
1902
|
+
return { files, unreadable, symlinks };
|
|
1832
1903
|
};
|
|
1833
1904
|
|
|
1834
1905
|
// src/fix/verify.ts
|
|
@@ -1863,29 +1934,47 @@ var sameDiagrams = async (before, after) => {
|
|
|
1863
1934
|
if (!now || was.keyword !== now.keyword)
|
|
1864
1935
|
return false;
|
|
1865
1936
|
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1866
|
-
const shape = (s) => [
|
|
1937
|
+
const shape = (s, source) => [
|
|
1938
|
+
parseFailed(source, s),
|
|
1867
1939
|
s.diagram_type,
|
|
1868
1940
|
s.nodes,
|
|
1869
1941
|
s.edges,
|
|
1870
1942
|
s.subgraphs,
|
|
1871
1943
|
s.max_subgraph_depth,
|
|
1944
|
+
s.max_tree_depth,
|
|
1872
1945
|
s.node_ids,
|
|
1873
1946
|
s.subgraph_names
|
|
1874
1947
|
];
|
|
1875
|
-
if (!sameJson(shape(a), shape(b)))
|
|
1948
|
+
if (!sameJson(shape(a, was.source), shape(b, now.source)))
|
|
1876
1949
|
return false;
|
|
1877
1950
|
}
|
|
1878
1951
|
return true;
|
|
1879
1952
|
};
|
|
1953
|
+
var breaksAParse = async (before, after) => {
|
|
1954
|
+
for (const [i, was] of before.diagrams.entries()) {
|
|
1955
|
+
const now = after.diagrams[i];
|
|
1956
|
+
if (!now)
|
|
1957
|
+
continue;
|
|
1958
|
+
const [a, b] = await Promise.all([was.stats(), now.stats()]);
|
|
1959
|
+
if (!parseFailed(was.source, a) && parseFailed(now.source, b))
|
|
1960
|
+
return true;
|
|
1961
|
+
}
|
|
1962
|
+
return false;
|
|
1963
|
+
};
|
|
1880
1964
|
var verify = async (edit, before, after) => {
|
|
1881
1965
|
if (edit.expect.kind === "same-tree") {
|
|
1882
|
-
|
|
1966
|
+
if (!sameTree(before.tree, after.tree, {}))
|
|
1967
|
+
return "refuse";
|
|
1968
|
+
if (before.kind === "markdown" && after.kind === "markdown") {
|
|
1969
|
+
return await breaksAParse(before, after) ? "refuse" : "accept";
|
|
1970
|
+
}
|
|
1883
1971
|
}
|
|
1884
1972
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1885
1973
|
};
|
|
1886
1974
|
|
|
1887
1975
|
// src/fix/engine.ts
|
|
1888
1976
|
var MAX_PASSES = 1000;
|
|
1977
|
+
var RECENT_PASSES = 50;
|
|
1889
1978
|
var splice = (src, edits) => {
|
|
1890
1979
|
let out = src;
|
|
1891
1980
|
for (const e of [...edits].sort((a, b) => b.start - a.start))
|
|
@@ -1903,6 +1992,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
|
1903
1992
|
const refused = [];
|
|
1904
1993
|
const refusedKeys = new Set;
|
|
1905
1994
|
let current = src;
|
|
1995
|
+
const keptBy = [];
|
|
1906
1996
|
for (let pass = 0;pass < MAX_PASSES; pass++) {
|
|
1907
1997
|
const doc = buildDocModel(current, kind);
|
|
1908
1998
|
const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
|
|
@@ -1935,23 +2025,26 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
|
1935
2025
|
continue;
|
|
1936
2026
|
if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
|
|
1937
2027
|
progressed = true;
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2028
|
+
} else {
|
|
2029
|
+
for (const edit of edits) {
|
|
2030
|
+
if (await attempt([edit])) {
|
|
2031
|
+
progressed = true;
|
|
2032
|
+
break;
|
|
2033
|
+
}
|
|
2034
|
+
refusedKeys.add(keyOf(edit));
|
|
2035
|
+
refused.push(edit);
|
|
1944
2036
|
}
|
|
1945
|
-
refusedKeys.add(keyOf(edit));
|
|
1946
|
-
refused.push(edit);
|
|
1947
2037
|
}
|
|
1948
|
-
if (progressed)
|
|
2038
|
+
if (progressed) {
|
|
2039
|
+
keptBy.push(rule.id);
|
|
1949
2040
|
break;
|
|
2041
|
+
}
|
|
1950
2042
|
}
|
|
1951
2043
|
if (!progressed)
|
|
1952
2044
|
return { output: current, applied, refused };
|
|
1953
2045
|
}
|
|
1954
|
-
|
|
2046
|
+
const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
|
|
2047
|
+
throw new FixNotConvergingError(file, MAX_PASSES, lingering);
|
|
1955
2048
|
};
|
|
1956
2049
|
|
|
1957
2050
|
// src/project.ts
|
|
@@ -2053,7 +2146,7 @@ var readJson = async (path) => {
|
|
|
2053
2146
|
if (isRecord2(value))
|
|
2054
2147
|
return value;
|
|
2055
2148
|
} catch {}
|
|
2056
|
-
throw new
|
|
2149
|
+
throw new PackageJsonError(path);
|
|
2057
2150
|
};
|
|
2058
2151
|
var LOCAL_EXTENSIONS = new Set([".js", ".mjs", ".ts", ".mts", ".cts"]);
|
|
2059
2152
|
var localSources = async (root) => {
|
|
@@ -2097,7 +2190,14 @@ var entryOf = (pkg) => {
|
|
|
2097
2190
|
var resolvePackage = async (root, name) => {
|
|
2098
2191
|
for (let dir = resolve3(root);; dir = dirname2(dir)) {
|
|
2099
2192
|
const folder = join4(dir, "node_modules", name);
|
|
2100
|
-
|
|
2193
|
+
let pkg;
|
|
2194
|
+
try {
|
|
2195
|
+
pkg = await readJson(join4(folder, "package.json"));
|
|
2196
|
+
} catch (err) {
|
|
2197
|
+
if (!(err instanceof PackageJsonError))
|
|
2198
|
+
throw err;
|
|
2199
|
+
throw new PluginSourceError(name, `its package.json at ${err.file} is not a JSON object`);
|
|
2200
|
+
}
|
|
2101
2201
|
if (pkg)
|
|
2102
2202
|
return join4(folder, entryOf(pkg));
|
|
2103
2203
|
if (dirname2(dir) === dir)
|
|
@@ -2105,8 +2205,15 @@ var resolvePackage = async (root, name) => {
|
|
|
2105
2205
|
}
|
|
2106
2206
|
throw new PluginNotInstalledError(name, root);
|
|
2107
2207
|
};
|
|
2108
|
-
var declaredPlugins = async (root) => {
|
|
2109
|
-
|
|
2208
|
+
var declaredPlugins = async (root, tolerateBadManifest) => {
|
|
2209
|
+
let pkg;
|
|
2210
|
+
try {
|
|
2211
|
+
pkg = await readJson(join4(root, "package.json"));
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
if (tolerateBadManifest && err instanceof PackageJsonError)
|
|
2214
|
+
return [];
|
|
2215
|
+
throw err;
|
|
2216
|
+
}
|
|
2110
2217
|
if (!pkg)
|
|
2111
2218
|
return [];
|
|
2112
2219
|
const names = ["dependencies", "devDependencies", "optionalDependencies"].flatMap((field) => {
|
|
@@ -2115,19 +2222,19 @@ var declaredPlugins = async (root) => {
|
|
|
2115
2222
|
});
|
|
2116
2223
|
return [...new Set(names)].filter((name) => PACKAGE_PATTERN.test(name)).sort();
|
|
2117
2224
|
};
|
|
2118
|
-
var discoverSources = async (root, config) => {
|
|
2225
|
+
var discoverSources = async (root, config, pluginsOff = false) => {
|
|
2119
2226
|
const sources = await localSources(root);
|
|
2120
2227
|
const failures = [];
|
|
2121
2228
|
const add = async (kind, spec, resolveIt) => {
|
|
2122
2229
|
try {
|
|
2123
2230
|
sources.push({ kind, spec, path: await resolveIt() });
|
|
2124
2231
|
} catch (err) {
|
|
2125
|
-
if (!(err instanceof
|
|
2232
|
+
if (!(err instanceof PluginLoadError))
|
|
2126
2233
|
throw err;
|
|
2127
|
-
failures.push(err);
|
|
2234
|
+
failures.push({ kind, error: err });
|
|
2128
2235
|
}
|
|
2129
2236
|
};
|
|
2130
|
-
for (const name of await declaredPlugins(root))
|
|
2237
|
+
for (const name of await declaredPlugins(root, pluginsOff))
|
|
2131
2238
|
await add("package", name, () => resolvePackage(root, name));
|
|
2132
2239
|
for (const spec of config.plugins || []) {
|
|
2133
2240
|
const path = spec.startsWith(".") || isAbsolute(spec);
|
|
@@ -2144,8 +2251,8 @@ import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
|
2144
2251
|
// src/plugins/contract.ts
|
|
2145
2252
|
var API_VERSION = 1;
|
|
2146
2253
|
var CATEGORY_WORD = /^[a-z]+$/;
|
|
2147
|
-
var NAMESPACE = /^[a-z][a-z0-9-]*$/;
|
|
2148
|
-
var RULE_KEY = /^[a-z][a-z0-9-]*$/;
|
|
2254
|
+
var NAMESPACE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2255
|
+
var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2149
2256
|
|
|
2150
2257
|
// src/plugins/runtime.ts
|
|
2151
2258
|
var canImportTypeScript = (runtime = process) => typeof runtime.versions.bun === "string" || Boolean(runtime.features.typescript);
|
|
@@ -2172,6 +2279,7 @@ var importModule = async (source, runtime) => {
|
|
|
2172
2279
|
}
|
|
2173
2280
|
};
|
|
2174
2281
|
var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]"]);
|
|
2282
|
+
var RESERVED_OPTIONS = ["preset", "profile"];
|
|
2175
2283
|
var checkRule = (spec, key, raw, own) => {
|
|
2176
2284
|
const where = `rule "${key}"`;
|
|
2177
2285
|
if (!isRecord3(raw))
|
|
@@ -2201,6 +2309,11 @@ var checkRule = (spec, key, raw, own) => {
|
|
|
2201
2309
|
if (!valid) {
|
|
2202
2310
|
throw new PluginContractError(spec, `${where} options must map names to number, string, boolean or string[]`);
|
|
2203
2311
|
}
|
|
2312
|
+
for (const name of RESERVED_OPTIONS) {
|
|
2313
|
+
if (name in options && options[name] !== "string") {
|
|
2314
|
+
throw new PluginContractError(spec, `${where} declares option "${name}" as ${String(options[name])}, but "${name}" is reserved for the built-in ${name}, which is a string set by --${name}; declare it as "string" to follow the run's ${name}, or pick another name`);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2204
2317
|
}
|
|
2205
2318
|
return raw;
|
|
2206
2319
|
};
|
|
@@ -2252,7 +2365,18 @@ var loadLocal = async (sources, runtime) => {
|
|
|
2252
2365
|
const categories = {};
|
|
2253
2366
|
const owner = {};
|
|
2254
2367
|
const loaded = [];
|
|
2368
|
+
const keyOf = (source) => basename(source.path, extname4(source.path));
|
|
2369
|
+
const byKey = new Map;
|
|
2370
|
+
for (const source of sources)
|
|
2371
|
+
byKey.set(keyOf(source), [...byKey.get(keyOf(source)) ?? [], source]);
|
|
2372
|
+
for (const [key, group] of byKey) {
|
|
2373
|
+
if (group.length < 2)
|
|
2374
|
+
continue;
|
|
2375
|
+
fail(new PluginContractError(group[0].spec, `rule "${key}" is defined by more than one file: ${group.map((s) => s.spec).join(", ")}; each rule comes from exactly one file, so rename or remove all but one`));
|
|
2376
|
+
}
|
|
2255
2377
|
for (const source of sources) {
|
|
2378
|
+
if ((byKey.get(keyOf(source))?.length ?? 0) > 1)
|
|
2379
|
+
continue;
|
|
2256
2380
|
try {
|
|
2257
2381
|
const mod = await importModule(source, runtime);
|
|
2258
2382
|
if (mod.default === undefined) {
|
|
@@ -2266,7 +2390,7 @@ var loadLocal = async (sources, runtime) => {
|
|
|
2266
2390
|
categories[word] = description;
|
|
2267
2391
|
owner[word] = source.spec;
|
|
2268
2392
|
}
|
|
2269
|
-
loaded.push({ key:
|
|
2393
|
+
loaded.push({ key: keyOf(source), source, raw: mod.default });
|
|
2270
2394
|
} catch (err) {
|
|
2271
2395
|
fail(err);
|
|
2272
2396
|
}
|
|
@@ -2330,7 +2454,7 @@ var toRule = (namespace, key, rule) => {
|
|
|
2330
2454
|
|
|
2331
2455
|
// src/plugins/index.ts
|
|
2332
2456
|
var loadPlugins = async (root, config, mode) => {
|
|
2333
|
-
const { sources: found, failures: unresolved } = await discoverSources(root, config);
|
|
2457
|
+
const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
|
|
2334
2458
|
const set = {
|
|
2335
2459
|
rules: [],
|
|
2336
2460
|
categories: new Map(BUILTIN_CATEGORIES),
|
|
@@ -2338,16 +2462,17 @@ var loadPlugins = async (root, config, mode) => {
|
|
|
2338
2462
|
skipped: [],
|
|
2339
2463
|
failures: []
|
|
2340
2464
|
};
|
|
2341
|
-
if (mode === "off")
|
|
2342
|
-
return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((
|
|
2465
|
+
if (mode === "off") {
|
|
2466
|
+
return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((u) => u.error.source)] };
|
|
2467
|
+
}
|
|
2343
2468
|
const settle = (error, kind) => {
|
|
2344
2469
|
if (mode === "strict")
|
|
2345
2470
|
throw error;
|
|
2346
2471
|
if (!set.failures.some((f) => f.source === error.source))
|
|
2347
2472
|
set.failures.push({ source: error.source, kind, error });
|
|
2348
2473
|
};
|
|
2349
|
-
for (const error of unresolved)
|
|
2350
|
-
settle(error,
|
|
2474
|
+
for (const { error, kind } of unresolved)
|
|
2475
|
+
settle(error, kind);
|
|
2351
2476
|
const loaded = [];
|
|
2352
2477
|
const locals = found.filter((s) => s.kind === "local");
|
|
2353
2478
|
if (locals.length > 0) {
|
|
@@ -2444,7 +2569,7 @@ var setUp = async ({
|
|
|
2444
2569
|
config,
|
|
2445
2570
|
ignoreUnknownPluginIds: mode !== "strict"
|
|
2446
2571
|
});
|
|
2447
|
-
return { registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2572
|
+
return { root, registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2448
2573
|
};
|
|
2449
2574
|
|
|
2450
2575
|
// src/cli.ts
|
|
@@ -2453,17 +2578,21 @@ var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME]
|
|
|
2453
2578
|
mermaidjs-lint --list-rules
|
|
2454
2579
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
2455
2580
|
mermaidjs-lint --version | -h, --help`;
|
|
2581
|
+
var ruleRow = (r, width) => `${`${r.id}${r.fix ? "*" : ""}`.padEnd(width)} ${r.summary}`;
|
|
2582
|
+
var idWidth = (rules) => Math.max(0, ...rules.map((r) => r.id.length + 1));
|
|
2456
2583
|
var USAGE = `${SYNOPSIS}
|
|
2457
2584
|
|
|
2458
2585
|
Deterministic Mermaid diagram gates (* = autofixable):
|
|
2459
|
-
${CATEGORIES.map((c) => [` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${r
|
|
2586
|
+
${CATEGORIES.map((c) => [` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${ruleRow(r, idWidth(RULES))}`)].join(`
|
|
2460
2587
|
`)).join(`
|
|
2461
2588
|
`)}
|
|
2462
2589
|
|
|
2463
2590
|
A path is a file or a directory, and every path must exist. A file ending in
|
|
2464
2591
|
${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
|
|
2465
2592
|
whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
|
|
2466
|
-
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}.
|
|
2593
|
+
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}. The walk does not follow a symbolic link,
|
|
2594
|
+
and names each one it skipped. A link named as a path is read, but --fix does not write
|
|
2595
|
+
through one whose target is outside the project root.
|
|
2467
2596
|
Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
|
|
2468
2597
|
|
|
2469
2598
|
-p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
|
|
@@ -2474,14 +2603,16 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
2474
2603
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
2475
2604
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
2476
2605
|
--config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
|
|
2477
|
-
Plugins load on their own from .mermaidjs-lint/rules
|
|
2606
|
+
Plugins load on their own from .mermaidjs-lint/rules, from dependencies named mermaidjs-lint-plugin,
|
|
2607
|
+
mermaidjs-lint-plugin-* or @scope/mermaidjs-lint-plugin(-*), and from the config's plugins list.
|
|
2478
2608
|
--no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
|
|
2479
2609
|
and stops with a usage error rather than import a .mjs config, which is code.
|
|
2480
2610
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
2481
|
-
--validate-plugins
|
|
2611
|
+
--validate-plugins imports and checks every plugin, reports every failure, and runs no check or fix.
|
|
2482
2612
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
2613
|
+
It imports the plugins to list them.
|
|
2483
2614
|
--version prints the version, to quote in a bug report.
|
|
2484
|
-
Exits 0 when no findings remain, 1 when findings remain
|
|
2615
|
+
Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
|
|
2485
2616
|
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
2486
2617
|
var parse2 = (argv) => parseArgs({
|
|
2487
2618
|
args: argv,
|
|
@@ -2517,13 +2648,14 @@ var skippedLines = (failures, ignored) => [
|
|
|
2517
2648
|
];
|
|
2518
2649
|
var listRules = (registry) => {
|
|
2519
2650
|
const lines = [];
|
|
2651
|
+
const width = idWidth(registry.rules);
|
|
2520
2652
|
for (const [category, description] of registry.categories) {
|
|
2521
2653
|
const rules = registry.rules.filter((r) => r.category === category);
|
|
2522
2654
|
if (rules.length === 0)
|
|
2523
2655
|
continue;
|
|
2524
2656
|
lines.push(`${category}: ${description}`);
|
|
2525
2657
|
for (const r of rules)
|
|
2526
|
-
lines.push(` ${r
|
|
2658
|
+
lines.push(` ${ruleRow(r, width)}`);
|
|
2527
2659
|
}
|
|
2528
2660
|
if (registry.sources.length > 0) {
|
|
2529
2661
|
lines.push("", "plugins:");
|
|
@@ -2566,6 +2698,17 @@ var display = (file, cwd) => {
|
|
|
2566
2698
|
const rel = relative(cwd, file);
|
|
2567
2699
|
return !rel || rel.startsWith("..") ? file : rel;
|
|
2568
2700
|
};
|
|
2701
|
+
var refuseOutsideLinks = async (files, root, cwd) => {
|
|
2702
|
+
const realRoot = await realpath(root);
|
|
2703
|
+
for (const file of files) {
|
|
2704
|
+
if (!(await lstat(file)).isSymbolicLink())
|
|
2705
|
+
continue;
|
|
2706
|
+
const target = await realpath(file);
|
|
2707
|
+
const rel = relative(realRoot, target);
|
|
2708
|
+
if (rel.startsWith("..") || isAbsolute2(rel))
|
|
2709
|
+
throw new FixOutsideRootError(display(file, cwd), target, root);
|
|
2710
|
+
}
|
|
2711
|
+
};
|
|
2569
2712
|
var run = async (argv, cwd) => {
|
|
2570
2713
|
let parsed;
|
|
2571
2714
|
try {
|
|
@@ -2601,7 +2744,7 @@ var run = async (argv, cwd) => {
|
|
|
2601
2744
|
}
|
|
2602
2745
|
if (positionals.length === 0 && !listing && !validating)
|
|
2603
2746
|
throw new FlagError("files", "no files given");
|
|
2604
|
-
const { registry, skipped, failures } = await setUp({
|
|
2747
|
+
const { root, registry, skipped, failures } = await setUp({
|
|
2605
2748
|
cwd,
|
|
2606
2749
|
configPath: values.config,
|
|
2607
2750
|
noPlugins: values["no-plugins"],
|
|
@@ -2625,11 +2768,16 @@ var run = async (argv, cwd) => {
|
|
|
2625
2768
|
console.log(listRules(registry));
|
|
2626
2769
|
return 0;
|
|
2627
2770
|
}
|
|
2628
|
-
const { files, unreadable } = collectFiles(positionals.map((p) =>
|
|
2771
|
+
const { files, unreadable, symlinks } = collectFiles(positionals.map((p) => resolve5(cwd, p)));
|
|
2629
2772
|
if (unreadable.length > 0)
|
|
2630
2773
|
throw new FlagError("files", `cannot read ${unreadable.join(", ")}`);
|
|
2774
|
+
for (const link of symlinks) {
|
|
2775
|
+
console.error(`mermaidjs-lint: skipped symbolic link ${display(link, cwd)}; name it as a path to check it`);
|
|
2776
|
+
}
|
|
2631
2777
|
if (files.length === 0)
|
|
2632
2778
|
throw new FlagError("files", "no matching files");
|
|
2779
|
+
if (values.fix)
|
|
2780
|
+
await refuseOutsideLinks(files, root, cwd);
|
|
2633
2781
|
const flags = { preset: values.preset, profile: values.profile };
|
|
2634
2782
|
const perFile = await Promise.all(files.map(async (file) => {
|
|
2635
2783
|
let src = await readFile3(file, "utf8");
|
|
@@ -2640,7 +2788,9 @@ var run = async (argv, cwd) => {
|
|
|
2640
2788
|
src = fixed;
|
|
2641
2789
|
}
|
|
2642
2790
|
}
|
|
2643
|
-
|
|
2791
|
+
const shown = display(file, cwd);
|
|
2792
|
+
const found = await checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
|
|
2793
|
+
return found.map((f) => ({ ...f, file: shown }));
|
|
2644
2794
|
}));
|
|
2645
2795
|
const all = perFile.flat();
|
|
2646
2796
|
if (values.json) {
|