@jpeakai/mermaidjs-lint 0.2.0 → 0.3.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 +7 -5
- package/dist/bin.js +174 -55
- package/dist/index.js +177 -55
- package/dist/testing.js +59 -23
- 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 +24 -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 +6 -1
- package/src/index.ts +5 -3
- package/src/parse/stats.ts +39 -11
- 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/README.md
CHANGED
|
@@ -70,17 +70,19 @@ It does not compare edge endpoints, edge labels or node text, so review the diff
|
|
|
70
70
|
|
|
71
71
|
[`skills/mermaidjs-lint/`](skills/mermaidjs-lint/SKILL.md) is an agent skill that runs the linter, applies the safe fixes, then fixes what remains by judgement.
|
|
72
72
|
It is not in the npm package, so copy it from this repository.
|
|
73
|
+
The skill is the one file `SKILL.md`.
|
|
74
|
+
Its `evals/` folder holds deliberately failing fixtures for this repository's own tests, so never copy it.
|
|
73
75
|
For Claude Code, run these in your project:
|
|
74
76
|
|
|
75
77
|
```sh
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
mkdir -p .claude/skills/mermaidjs-lint
|
|
79
|
+
curl -fsSL https://raw.githubusercontent.com/jpeakai/jpai-mermaidjs-lint/main/skills/mermaidjs-lint/SKILL.md \
|
|
80
|
+
-o .claude/skills/mermaidjs-lint/SKILL.md
|
|
79
81
|
```
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
Use `~/.claude/skills/mermaidjs-lint` instead to use it in every project.
|
|
82
84
|
Another agent can follow `SKILL.md` as plain instructions.
|
|
83
|
-
[Agent evals](https://jpeakai.github.io/jpai-mermaidjs-lint/gallery/agent-evals/) shows how well agents
|
|
85
|
+
[Agent evals](https://jpeakai.github.io/jpai-mermaidjs-lint/gallery/agent-evals/) shows how well agents fixed the failing fixtures it lists, and which skill revision they used.
|
|
84
86
|
|
|
85
87
|
## Library use
|
|
86
88
|
|
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.3.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
|
};
|
|
@@ -624,12 +625,10 @@ function adaptMindmap(db) {
|
|
|
624
625
|
}
|
|
625
626
|
function adaptQuadrantChart(db) {
|
|
626
627
|
const data = callGetter(db, "getQuadrantData");
|
|
627
|
-
const
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
});
|
|
632
|
-
return { nodeIds: labels, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
628
|
+
const textOf = (t) => typeof t === "string" ? t : t?.text ?? "";
|
|
629
|
+
const quadrants = (data?.quadrants ?? []).map((q) => textOf(q.text)).filter((t) => t.trim() !== "");
|
|
630
|
+
const points = (data?.points ?? []).map((p, i) => textOf(p.text) || `point_${i}`);
|
|
631
|
+
return { nodeIds: [...quadrants, ...points], edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
633
632
|
}
|
|
634
633
|
function adaptRequirement(db) {
|
|
635
634
|
const requirements = db.requirements instanceof Map ? db.requirements : new Map;
|
|
@@ -655,18 +654,30 @@ function adaptSankey(db) {
|
|
|
655
654
|
function adaptState(db) {
|
|
656
655
|
const nodes = db.nodes ?? [];
|
|
657
656
|
const edges = db.edges ?? [];
|
|
657
|
+
const groups = nodes.filter((n) => n.isGroup === true);
|
|
658
|
+
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
659
|
+
let maxDepth = 0;
|
|
660
|
+
for (const g of groups) {
|
|
661
|
+
let depth = 1;
|
|
662
|
+
const seen = new Set;
|
|
663
|
+
for (let cur = g.parentId;cur && !seen.has(cur); cur = parentOf.get(cur)) {
|
|
664
|
+
seen.add(cur);
|
|
665
|
+
depth++;
|
|
666
|
+
}
|
|
667
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
668
|
+
}
|
|
658
669
|
return {
|
|
659
670
|
nodeIds: nodes.map((n) => n.id ?? ""),
|
|
660
671
|
edges: edges.length,
|
|
661
|
-
subgraphNames:
|
|
662
|
-
maxDepth
|
|
672
|
+
subgraphNames: groups.map((g) => g.id ?? ""),
|
|
673
|
+
maxDepth
|
|
663
674
|
};
|
|
664
675
|
}
|
|
665
676
|
function adaptTimeline(db) {
|
|
666
677
|
const tasks = callGetter(db, "getTasks") ?? [];
|
|
667
678
|
const sections = callGetter(db, "getSections") ?? [];
|
|
668
679
|
return {
|
|
669
|
-
nodeIds: tasks.
|
|
680
|
+
nodeIds: tasks.flatMap((t) => [t.task ?? "", ...(t.events ?? []).map((e) => `${t.task ?? ""}: ${e}`)]),
|
|
670
681
|
edges: 0,
|
|
671
682
|
subgraphNames: sections.map((s) => s.sectionName ?? s.section ?? ""),
|
|
672
683
|
maxDepth: sections.length > 0 ? 1 : 0
|
|
@@ -696,7 +707,10 @@ function adaptVenn(db) {
|
|
|
696
707
|
function adaptXychart(db) {
|
|
697
708
|
const data = callGetter(db, "getXYChartData");
|
|
698
709
|
const categories = data?.xAxis?.categories ?? [];
|
|
699
|
-
|
|
710
|
+
if (categories.length > 0)
|
|
711
|
+
return { nodeIds: categories, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
712
|
+
const longest = Math.max(0, ...(data?.plots ?? []).map((p) => p.data?.length ?? 0));
|
|
713
|
+
return { nodeIds: Array.from({ length: longest }, (_, i) => `x_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
700
714
|
}
|
|
701
715
|
function subgraphNestingDepth(subGraphs) {
|
|
702
716
|
if (subGraphs.length === 0)
|
|
@@ -923,6 +937,34 @@ class RuleOptionError extends UsageError {
|
|
|
923
937
|
}
|
|
924
938
|
}
|
|
925
939
|
|
|
940
|
+
class FixOutsideRootError extends UsageError {
|
|
941
|
+
file;
|
|
942
|
+
target;
|
|
943
|
+
constructor(file, target, root) {
|
|
944
|
+
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 name it directly`, "fix-outside-root");
|
|
945
|
+
this.file = file;
|
|
946
|
+
this.target = target;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
class PackageJsonError extends MermaidLintError {
|
|
951
|
+
file;
|
|
952
|
+
constructor(file) {
|
|
953
|
+
super("package-json", `${file} is not a JSON object; fix or remove it`);
|
|
954
|
+
this.file = file;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
class FixNotConvergingError extends MermaidLintError {
|
|
959
|
+
file;
|
|
960
|
+
rules;
|
|
961
|
+
constructor(file, passes, rules) {
|
|
962
|
+
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`);
|
|
963
|
+
this.file = file;
|
|
964
|
+
this.rules = rules;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
926
968
|
class PluginLoadError extends MermaidLintError {
|
|
927
969
|
source;
|
|
928
970
|
constructor(source, reason, code = "plugin") {
|
|
@@ -1789,9 +1831,17 @@ import { extname, join as join2, resolve as resolve2 } from "node:path";
|
|
|
1789
1831
|
var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
|
|
1790
1832
|
var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
|
|
1791
1833
|
var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
|
|
1834
|
+
var linksToDir = (path) => {
|
|
1835
|
+
try {
|
|
1836
|
+
return statSync(path).isDirectory();
|
|
1837
|
+
} catch {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
};
|
|
1792
1841
|
var collectFiles = (paths) => {
|
|
1793
1842
|
const files = [];
|
|
1794
1843
|
const unreadable = [];
|
|
1844
|
+
const symlinks = [];
|
|
1795
1845
|
const seen = new Set;
|
|
1796
1846
|
const walk = (dir) => {
|
|
1797
1847
|
let entries;
|
|
@@ -1803,7 +1853,11 @@ var collectFiles = (paths) => {
|
|
|
1803
1853
|
}
|
|
1804
1854
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1805
1855
|
const abs = join2(dir, entry.name);
|
|
1806
|
-
if (entry.
|
|
1856
|
+
if (entry.isSymbolicLink()) {
|
|
1857
|
+
if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
|
|
1858
|
+
symlinks.push(abs);
|
|
1859
|
+
}
|
|
1860
|
+
} else if (entry.isDirectory()) {
|
|
1807
1861
|
if (!skipDir(entry.name))
|
|
1808
1862
|
walk(abs);
|
|
1809
1863
|
} else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
|
|
@@ -1828,7 +1882,7 @@ var collectFiles = (paths) => {
|
|
|
1828
1882
|
unreadable.push(p);
|
|
1829
1883
|
}
|
|
1830
1884
|
}
|
|
1831
|
-
return { files, unreadable };
|
|
1885
|
+
return { files, unreadable, symlinks };
|
|
1832
1886
|
};
|
|
1833
1887
|
|
|
1834
1888
|
// src/fix/verify.ts
|
|
@@ -1879,13 +1933,17 @@ var sameDiagrams = async (before, after) => {
|
|
|
1879
1933
|
};
|
|
1880
1934
|
var verify = async (edit, before, after) => {
|
|
1881
1935
|
if (edit.expect.kind === "same-tree") {
|
|
1882
|
-
|
|
1936
|
+
if (!sameTree(before.tree, after.tree, {}))
|
|
1937
|
+
return "refuse";
|
|
1938
|
+
if (before.kind === "markdown" && after.kind === "markdown")
|
|
1939
|
+
return "accept";
|
|
1883
1940
|
}
|
|
1884
1941
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1885
1942
|
};
|
|
1886
1943
|
|
|
1887
1944
|
// src/fix/engine.ts
|
|
1888
1945
|
var MAX_PASSES = 1000;
|
|
1946
|
+
var RECENT_PASSES = 50;
|
|
1889
1947
|
var splice = (src, edits) => {
|
|
1890
1948
|
let out = src;
|
|
1891
1949
|
for (const e of [...edits].sort((a, b) => b.start - a.start))
|
|
@@ -1903,6 +1961,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
|
1903
1961
|
const refused = [];
|
|
1904
1962
|
const refusedKeys = new Set;
|
|
1905
1963
|
let current = src;
|
|
1964
|
+
const keptBy = [];
|
|
1906
1965
|
for (let pass = 0;pass < MAX_PASSES; pass++) {
|
|
1907
1966
|
const doc = buildDocModel(current, kind);
|
|
1908
1967
|
const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
|
|
@@ -1935,23 +1994,26 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
|
1935
1994
|
continue;
|
|
1936
1995
|
if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
|
|
1937
1996
|
progressed = true;
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1997
|
+
} else {
|
|
1998
|
+
for (const edit of edits) {
|
|
1999
|
+
if (await attempt([edit])) {
|
|
2000
|
+
progressed = true;
|
|
2001
|
+
break;
|
|
2002
|
+
}
|
|
2003
|
+
refusedKeys.add(keyOf(edit));
|
|
2004
|
+
refused.push(edit);
|
|
1944
2005
|
}
|
|
1945
|
-
refusedKeys.add(keyOf(edit));
|
|
1946
|
-
refused.push(edit);
|
|
1947
2006
|
}
|
|
1948
|
-
if (progressed)
|
|
2007
|
+
if (progressed) {
|
|
2008
|
+
keptBy.push(rule.id);
|
|
1949
2009
|
break;
|
|
2010
|
+
}
|
|
1950
2011
|
}
|
|
1951
2012
|
if (!progressed)
|
|
1952
2013
|
return { output: current, applied, refused };
|
|
1953
2014
|
}
|
|
1954
|
-
|
|
2015
|
+
const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
|
|
2016
|
+
throw new FixNotConvergingError(file, MAX_PASSES, lingering);
|
|
1955
2017
|
};
|
|
1956
2018
|
|
|
1957
2019
|
// src/project.ts
|
|
@@ -2053,7 +2115,7 @@ var readJson = async (path) => {
|
|
|
2053
2115
|
if (isRecord2(value))
|
|
2054
2116
|
return value;
|
|
2055
2117
|
} catch {}
|
|
2056
|
-
throw new
|
|
2118
|
+
throw new PackageJsonError(path);
|
|
2057
2119
|
};
|
|
2058
2120
|
var LOCAL_EXTENSIONS = new Set([".js", ".mjs", ".ts", ".mts", ".cts"]);
|
|
2059
2121
|
var localSources = async (root) => {
|
|
@@ -2097,7 +2159,14 @@ var entryOf = (pkg) => {
|
|
|
2097
2159
|
var resolvePackage = async (root, name) => {
|
|
2098
2160
|
for (let dir = resolve3(root);; dir = dirname2(dir)) {
|
|
2099
2161
|
const folder = join4(dir, "node_modules", name);
|
|
2100
|
-
|
|
2162
|
+
let pkg;
|
|
2163
|
+
try {
|
|
2164
|
+
pkg = await readJson(join4(folder, "package.json"));
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
if (!(err instanceof PackageJsonError))
|
|
2167
|
+
throw err;
|
|
2168
|
+
throw new PluginSourceError(name, `its package.json at ${err.file} is not a JSON object`);
|
|
2169
|
+
}
|
|
2101
2170
|
if (pkg)
|
|
2102
2171
|
return join4(folder, entryOf(pkg));
|
|
2103
2172
|
if (dirname2(dir) === dir)
|
|
@@ -2105,8 +2174,15 @@ var resolvePackage = async (root, name) => {
|
|
|
2105
2174
|
}
|
|
2106
2175
|
throw new PluginNotInstalledError(name, root);
|
|
2107
2176
|
};
|
|
2108
|
-
var declaredPlugins = async (root) => {
|
|
2109
|
-
|
|
2177
|
+
var declaredPlugins = async (root, tolerateBadManifest) => {
|
|
2178
|
+
let pkg;
|
|
2179
|
+
try {
|
|
2180
|
+
pkg = await readJson(join4(root, "package.json"));
|
|
2181
|
+
} catch (err) {
|
|
2182
|
+
if (tolerateBadManifest && err instanceof PackageJsonError)
|
|
2183
|
+
return [];
|
|
2184
|
+
throw err;
|
|
2185
|
+
}
|
|
2110
2186
|
if (!pkg)
|
|
2111
2187
|
return [];
|
|
2112
2188
|
const names = ["dependencies", "devDependencies", "optionalDependencies"].flatMap((field) => {
|
|
@@ -2115,19 +2191,19 @@ var declaredPlugins = async (root) => {
|
|
|
2115
2191
|
});
|
|
2116
2192
|
return [...new Set(names)].filter((name) => PACKAGE_PATTERN.test(name)).sort();
|
|
2117
2193
|
};
|
|
2118
|
-
var discoverSources = async (root, config) => {
|
|
2194
|
+
var discoverSources = async (root, config, pluginsOff = false) => {
|
|
2119
2195
|
const sources = await localSources(root);
|
|
2120
2196
|
const failures = [];
|
|
2121
2197
|
const add = async (kind, spec, resolveIt) => {
|
|
2122
2198
|
try {
|
|
2123
2199
|
sources.push({ kind, spec, path: await resolveIt() });
|
|
2124
2200
|
} catch (err) {
|
|
2125
|
-
if (!(err instanceof
|
|
2201
|
+
if (!(err instanceof PluginLoadError))
|
|
2126
2202
|
throw err;
|
|
2127
|
-
failures.push(err);
|
|
2203
|
+
failures.push({ kind, error: err });
|
|
2128
2204
|
}
|
|
2129
2205
|
};
|
|
2130
|
-
for (const name of await declaredPlugins(root))
|
|
2206
|
+
for (const name of await declaredPlugins(root, pluginsOff))
|
|
2131
2207
|
await add("package", name, () => resolvePackage(root, name));
|
|
2132
2208
|
for (const spec of config.plugins || []) {
|
|
2133
2209
|
const path = spec.startsWith(".") || isAbsolute(spec);
|
|
@@ -2144,8 +2220,8 @@ import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
|
2144
2220
|
// src/plugins/contract.ts
|
|
2145
2221
|
var API_VERSION = 1;
|
|
2146
2222
|
var CATEGORY_WORD = /^[a-z]+$/;
|
|
2147
|
-
var NAMESPACE = /^[a-z][a-z0-9-]*$/;
|
|
2148
|
-
var RULE_KEY = /^[a-z][a-z0-9-]*$/;
|
|
2223
|
+
var NAMESPACE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2224
|
+
var RULE_KEY = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2149
2225
|
|
|
2150
2226
|
// src/plugins/runtime.ts
|
|
2151
2227
|
var canImportTypeScript = (runtime = process) => typeof runtime.versions.bun === "string" || Boolean(runtime.features.typescript);
|
|
@@ -2172,6 +2248,7 @@ var importModule = async (source, runtime) => {
|
|
|
2172
2248
|
}
|
|
2173
2249
|
};
|
|
2174
2250
|
var OPTION_TYPES = new Set(["number", "string", "boolean", "string[]"]);
|
|
2251
|
+
var RESERVED_OPTIONS = ["preset", "profile"];
|
|
2175
2252
|
var checkRule = (spec, key, raw, own) => {
|
|
2176
2253
|
const where = `rule "${key}"`;
|
|
2177
2254
|
if (!isRecord3(raw))
|
|
@@ -2201,6 +2278,11 @@ var checkRule = (spec, key, raw, own) => {
|
|
|
2201
2278
|
if (!valid) {
|
|
2202
2279
|
throw new PluginContractError(spec, `${where} options must map names to number, string, boolean or string[]`);
|
|
2203
2280
|
}
|
|
2281
|
+
for (const name of RESERVED_OPTIONS) {
|
|
2282
|
+
if (name in options && options[name] !== "string") {
|
|
2283
|
+
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`);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2204
2286
|
}
|
|
2205
2287
|
return raw;
|
|
2206
2288
|
};
|
|
@@ -2252,7 +2334,18 @@ var loadLocal = async (sources, runtime) => {
|
|
|
2252
2334
|
const categories = {};
|
|
2253
2335
|
const owner = {};
|
|
2254
2336
|
const loaded = [];
|
|
2337
|
+
const keyOf = (source) => basename(source.path, extname4(source.path));
|
|
2338
|
+
const byKey = new Map;
|
|
2339
|
+
for (const source of sources)
|
|
2340
|
+
byKey.set(keyOf(source), [...byKey.get(keyOf(source)) ?? [], source]);
|
|
2341
|
+
for (const [key, group] of byKey) {
|
|
2342
|
+
if (group.length < 2)
|
|
2343
|
+
continue;
|
|
2344
|
+
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`));
|
|
2345
|
+
}
|
|
2255
2346
|
for (const source of sources) {
|
|
2347
|
+
if ((byKey.get(keyOf(source))?.length ?? 0) > 1)
|
|
2348
|
+
continue;
|
|
2256
2349
|
try {
|
|
2257
2350
|
const mod = await importModule(source, runtime);
|
|
2258
2351
|
if (mod.default === undefined) {
|
|
@@ -2266,7 +2359,7 @@ var loadLocal = async (sources, runtime) => {
|
|
|
2266
2359
|
categories[word] = description;
|
|
2267
2360
|
owner[word] = source.spec;
|
|
2268
2361
|
}
|
|
2269
|
-
loaded.push({ key:
|
|
2362
|
+
loaded.push({ key: keyOf(source), source, raw: mod.default });
|
|
2270
2363
|
} catch (err) {
|
|
2271
2364
|
fail(err);
|
|
2272
2365
|
}
|
|
@@ -2330,7 +2423,7 @@ var toRule = (namespace, key, rule) => {
|
|
|
2330
2423
|
|
|
2331
2424
|
// src/plugins/index.ts
|
|
2332
2425
|
var loadPlugins = async (root, config, mode) => {
|
|
2333
|
-
const { sources: found, failures: unresolved } = await discoverSources(root, config);
|
|
2426
|
+
const { sources: found, failures: unresolved } = await discoverSources(root, config, mode === "off");
|
|
2334
2427
|
const set = {
|
|
2335
2428
|
rules: [],
|
|
2336
2429
|
categories: new Map(BUILTIN_CATEGORIES),
|
|
@@ -2338,16 +2431,17 @@ var loadPlugins = async (root, config, mode) => {
|
|
|
2338
2431
|
skipped: [],
|
|
2339
2432
|
failures: []
|
|
2340
2433
|
};
|
|
2341
|
-
if (mode === "off")
|
|
2342
|
-
return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((
|
|
2434
|
+
if (mode === "off") {
|
|
2435
|
+
return { ...set, skipped: [...found.map((s) => s.spec), ...unresolved.map((u) => u.error.source)] };
|
|
2436
|
+
}
|
|
2343
2437
|
const settle = (error, kind) => {
|
|
2344
2438
|
if (mode === "strict")
|
|
2345
2439
|
throw error;
|
|
2346
2440
|
if (!set.failures.some((f) => f.source === error.source))
|
|
2347
2441
|
set.failures.push({ source: error.source, kind, error });
|
|
2348
2442
|
};
|
|
2349
|
-
for (const error of unresolved)
|
|
2350
|
-
settle(error,
|
|
2443
|
+
for (const { error, kind } of unresolved)
|
|
2444
|
+
settle(error, kind);
|
|
2351
2445
|
const loaded = [];
|
|
2352
2446
|
const locals = found.filter((s) => s.kind === "local");
|
|
2353
2447
|
if (locals.length > 0) {
|
|
@@ -2444,7 +2538,7 @@ var setUp = async ({
|
|
|
2444
2538
|
config,
|
|
2445
2539
|
ignoreUnknownPluginIds: mode !== "strict"
|
|
2446
2540
|
});
|
|
2447
|
-
return { registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2541
|
+
return { root, registry, skipped: plugins.skipped, failures: plugins.failures };
|
|
2448
2542
|
};
|
|
2449
2543
|
|
|
2450
2544
|
// src/cli.ts
|
|
@@ -2453,17 +2547,21 @@ var SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAME]
|
|
|
2453
2547
|
mermaidjs-lint --list-rules
|
|
2454
2548
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
2455
2549
|
mermaidjs-lint --version | -h, --help`;
|
|
2550
|
+
var ruleRow = (r, width) => `${`${r.id}${r.fix ? "*" : ""}`.padEnd(width)} ${r.summary}`;
|
|
2551
|
+
var idWidth = (rules) => Math.max(0, ...rules.map((r) => r.id.length + 1));
|
|
2456
2552
|
var USAGE = `${SYNOPSIS}
|
|
2457
2553
|
|
|
2458
2554
|
Deterministic Mermaid diagram gates (* = autofixable):
|
|
2459
|
-
${CATEGORIES.map((c) => [` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${r
|
|
2555
|
+
${CATEGORIES.map((c) => [` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${ruleRow(r, idWidth(RULES))}`)].join(`
|
|
2460
2556
|
`)).join(`
|
|
2461
2557
|
`)}
|
|
2462
2558
|
|
|
2463
2559
|
A path is a file or a directory, and every path must exist. A file ending in
|
|
2464
2560
|
${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
|
|
2465
2561
|
whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
|
|
2466
|
-
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}.
|
|
2562
|
+
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}. The walk does not follow a symbolic link,
|
|
2563
|
+
and names each one it skipped. A link named as a path is read, but --fix does not write
|
|
2564
|
+
through one whose target is outside the project root.
|
|
2467
2565
|
Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
|
|
2468
2566
|
|
|
2469
2567
|
-p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
|
|
@@ -2474,14 +2572,16 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
2474
2572
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
2475
2573
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
2476
2574
|
--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
|
|
2575
|
+
Plugins load on their own from .mermaidjs-lint/rules, from dependencies named mermaidjs-lint-plugin,
|
|
2576
|
+
mermaidjs-lint-plugin-* or @scope/mermaidjs-lint-plugin(-*), and from the config's plugins list.
|
|
2478
2577
|
--no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
|
|
2479
2578
|
and stops with a usage error rather than import a .mjs config, which is code.
|
|
2480
2579
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
2481
|
-
--validate-plugins
|
|
2580
|
+
--validate-plugins imports and checks every plugin, reports every failure, and runs no check or fix.
|
|
2482
2581
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
2582
|
+
It imports the plugins to list them.
|
|
2483
2583
|
--version prints the version, to quote in a bug report.
|
|
2484
|
-
Exits 0 when no findings remain, 1 when findings remain
|
|
2584
|
+
Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
|
|
2485
2585
|
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
2486
2586
|
var parse2 = (argv) => parseArgs({
|
|
2487
2587
|
args: argv,
|
|
@@ -2517,13 +2617,14 @@ var skippedLines = (failures, ignored) => [
|
|
|
2517
2617
|
];
|
|
2518
2618
|
var listRules = (registry) => {
|
|
2519
2619
|
const lines = [];
|
|
2620
|
+
const width = idWidth(registry.rules);
|
|
2520
2621
|
for (const [category, description] of registry.categories) {
|
|
2521
2622
|
const rules = registry.rules.filter((r) => r.category === category);
|
|
2522
2623
|
if (rules.length === 0)
|
|
2523
2624
|
continue;
|
|
2524
2625
|
lines.push(`${category}: ${description}`);
|
|
2525
2626
|
for (const r of rules)
|
|
2526
|
-
lines.push(` ${r
|
|
2627
|
+
lines.push(` ${ruleRow(r, width)}`);
|
|
2527
2628
|
}
|
|
2528
2629
|
if (registry.sources.length > 0) {
|
|
2529
2630
|
lines.push("", "plugins:");
|
|
@@ -2566,6 +2667,17 @@ var display = (file, cwd) => {
|
|
|
2566
2667
|
const rel = relative(cwd, file);
|
|
2567
2668
|
return !rel || rel.startsWith("..") ? file : rel;
|
|
2568
2669
|
};
|
|
2670
|
+
var refuseOutsideLinks = async (files, root, cwd) => {
|
|
2671
|
+
const realRoot = await realpath(root);
|
|
2672
|
+
for (const file of files) {
|
|
2673
|
+
if (!(await lstat(file)).isSymbolicLink())
|
|
2674
|
+
continue;
|
|
2675
|
+
const target = await realpath(file);
|
|
2676
|
+
const rel = relative(realRoot, target);
|
|
2677
|
+
if (rel.startsWith("..") || isAbsolute2(rel))
|
|
2678
|
+
throw new FixOutsideRootError(display(file, cwd), target, root);
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2569
2681
|
var run = async (argv, cwd) => {
|
|
2570
2682
|
let parsed;
|
|
2571
2683
|
try {
|
|
@@ -2601,7 +2713,7 @@ var run = async (argv, cwd) => {
|
|
|
2601
2713
|
}
|
|
2602
2714
|
if (positionals.length === 0 && !listing && !validating)
|
|
2603
2715
|
throw new FlagError("files", "no files given");
|
|
2604
|
-
const { registry, skipped, failures } = await setUp({
|
|
2716
|
+
const { root, registry, skipped, failures } = await setUp({
|
|
2605
2717
|
cwd,
|
|
2606
2718
|
configPath: values.config,
|
|
2607
2719
|
noPlugins: values["no-plugins"],
|
|
@@ -2625,11 +2737,16 @@ var run = async (argv, cwd) => {
|
|
|
2625
2737
|
console.log(listRules(registry));
|
|
2626
2738
|
return 0;
|
|
2627
2739
|
}
|
|
2628
|
-
const { files, unreadable } = collectFiles(positionals.map((p) =>
|
|
2740
|
+
const { files, unreadable, symlinks } = collectFiles(positionals.map((p) => resolve5(cwd, p)));
|
|
2629
2741
|
if (unreadable.length > 0)
|
|
2630
2742
|
throw new FlagError("files", `cannot read ${unreadable.join(", ")}`);
|
|
2743
|
+
for (const link of symlinks) {
|
|
2744
|
+
console.error(`mermaidjs-lint: skipped symbolic link ${display(link, cwd)}; name it as a path to check it`);
|
|
2745
|
+
}
|
|
2631
2746
|
if (files.length === 0)
|
|
2632
2747
|
throw new FlagError("files", "no matching files");
|
|
2748
|
+
if (values.fix)
|
|
2749
|
+
await refuseOutsideLinks(files, root, cwd);
|
|
2633
2750
|
const flags = { preset: values.preset, profile: values.profile };
|
|
2634
2751
|
const perFile = await Promise.all(files.map(async (file) => {
|
|
2635
2752
|
let src = await readFile3(file, "utf8");
|
|
@@ -2640,7 +2757,9 @@ var run = async (argv, cwd) => {
|
|
|
2640
2757
|
src = fixed;
|
|
2641
2758
|
}
|
|
2642
2759
|
}
|
|
2643
|
-
|
|
2760
|
+
const shown = display(file, cwd);
|
|
2761
|
+
const found = await checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
|
|
2762
|
+
return found.map((f) => ({ ...f, file: shown }));
|
|
2644
2763
|
}));
|
|
2645
2764
|
const all = perFile.flat();
|
|
2646
2765
|
if (values.json) {
|