@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/dist/index.js
CHANGED
|
@@ -547,12 +547,10 @@ function adaptMindmap(db) {
|
|
|
547
547
|
}
|
|
548
548
|
function adaptQuadrantChart(db) {
|
|
549
549
|
const data = callGetter(db, "getQuadrantData");
|
|
550
|
-
const
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
});
|
|
555
|
-
return { nodeIds: labels, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
550
|
+
const textOf = (t) => typeof t === "string" ? t : t?.text ?? "";
|
|
551
|
+
const quadrants = (data?.quadrants ?? []).map((q) => textOf(q.text)).filter((t) => t.trim() !== "");
|
|
552
|
+
const points = (data?.points ?? []).map((p, i) => textOf(p.text) || `point_${i}`);
|
|
553
|
+
return { nodeIds: [...quadrants, ...points], edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
556
554
|
}
|
|
557
555
|
function adaptRequirement(db) {
|
|
558
556
|
const requirements = db.requirements instanceof Map ? db.requirements : new Map;
|
|
@@ -578,18 +576,30 @@ function adaptSankey(db) {
|
|
|
578
576
|
function adaptState(db) {
|
|
579
577
|
const nodes = db.nodes ?? [];
|
|
580
578
|
const edges = db.edges ?? [];
|
|
579
|
+
const groups = nodes.filter((n) => n.isGroup === true);
|
|
580
|
+
const parentOf = new Map(nodes.map((n) => [n.id ?? "", n.parentId]));
|
|
581
|
+
let maxDepth = 0;
|
|
582
|
+
for (const g of groups) {
|
|
583
|
+
let depth = 1;
|
|
584
|
+
const seen = new Set;
|
|
585
|
+
for (let cur = g.parentId;cur && !seen.has(cur); cur = parentOf.get(cur)) {
|
|
586
|
+
seen.add(cur);
|
|
587
|
+
depth++;
|
|
588
|
+
}
|
|
589
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
590
|
+
}
|
|
581
591
|
return {
|
|
582
592
|
nodeIds: nodes.map((n) => n.id ?? ""),
|
|
583
593
|
edges: edges.length,
|
|
584
|
-
subgraphNames:
|
|
585
|
-
maxDepth
|
|
594
|
+
subgraphNames: groups.map((g) => g.id ?? ""),
|
|
595
|
+
maxDepth
|
|
586
596
|
};
|
|
587
597
|
}
|
|
588
598
|
function adaptTimeline(db) {
|
|
589
599
|
const tasks = callGetter(db, "getTasks") ?? [];
|
|
590
600
|
const sections = callGetter(db, "getSections") ?? [];
|
|
591
601
|
return {
|
|
592
|
-
nodeIds: tasks.
|
|
602
|
+
nodeIds: tasks.flatMap((t) => [t.task ?? "", ...(t.events ?? []).map((e) => `${t.task ?? ""}: ${e}`)]),
|
|
593
603
|
edges: 0,
|
|
594
604
|
subgraphNames: sections.map((s) => s.sectionName ?? s.section ?? ""),
|
|
595
605
|
maxDepth: sections.length > 0 ? 1 : 0
|
|
@@ -619,7 +629,10 @@ function adaptVenn(db) {
|
|
|
619
629
|
function adaptXychart(db) {
|
|
620
630
|
const data = callGetter(db, "getXYChartData");
|
|
621
631
|
const categories = data?.xAxis?.categories ?? [];
|
|
622
|
-
|
|
632
|
+
if (categories.length > 0)
|
|
633
|
+
return { nodeIds: categories, edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
634
|
+
const longest = Math.max(0, ...(data?.plots ?? []).map((p) => p.data?.length ?? 0));
|
|
635
|
+
return { nodeIds: Array.from({ length: longest }, (_, i) => `x_${i}`), edges: 0, subgraphNames: [], maxDepth: 0 };
|
|
623
636
|
}
|
|
624
637
|
function subgraphNestingDepth(subGraphs) {
|
|
625
638
|
if (subGraphs.length === 0)
|
|
@@ -846,6 +859,34 @@ class RuleOptionError extends UsageError {
|
|
|
846
859
|
}
|
|
847
860
|
}
|
|
848
861
|
|
|
862
|
+
class FixOutsideRootError extends UsageError {
|
|
863
|
+
file;
|
|
864
|
+
target;
|
|
865
|
+
constructor(file, target, root) {
|
|
866
|
+
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");
|
|
867
|
+
this.file = file;
|
|
868
|
+
this.target = target;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
class PackageJsonError extends MermaidLintError {
|
|
873
|
+
file;
|
|
874
|
+
constructor(file) {
|
|
875
|
+
super("package-json", `${file} is not a JSON object; fix or remove it`);
|
|
876
|
+
this.file = file;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
class FixNotConvergingError extends MermaidLintError {
|
|
881
|
+
file;
|
|
882
|
+
rules;
|
|
883
|
+
constructor(file, passes, rules) {
|
|
884
|
+
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`);
|
|
885
|
+
this.file = file;
|
|
886
|
+
this.rules = rules;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
849
890
|
class PluginLoadError extends MermaidLintError {
|
|
850
891
|
source;
|
|
851
892
|
constructor(source, reason, code = "plugin") {
|
|
@@ -1707,13 +1748,13 @@ var checkModel = async (doc, file, flags = {}, registry = BUILTIN) => {
|
|
|
1707
1748
|
};
|
|
1708
1749
|
var checkMarkdown = (src, file, flags = {}, registry = BUILTIN) => checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
|
|
1709
1750
|
// src/cli.ts
|
|
1710
|
-
import { readFile as readFile3, writeFile } from "node:fs/promises";
|
|
1711
|
-
import { relative } from "node:path";
|
|
1751
|
+
import { lstat, readFile as readFile3, realpath, writeFile } from "node:fs/promises";
|
|
1752
|
+
import { isAbsolute as isAbsolute2, relative, resolve as resolve5 } from "node:path";
|
|
1712
1753
|
import { parseArgs } from "node:util";
|
|
1713
1754
|
// package.json
|
|
1714
1755
|
var package_default = {
|
|
1715
1756
|
name: "@jpeakai/mermaidjs-lint",
|
|
1716
|
-
version: "0.
|
|
1757
|
+
version: "0.3.0",
|
|
1717
1758
|
type: "module",
|
|
1718
1759
|
description: "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
1719
1760
|
license: "MIT",
|
|
@@ -1742,15 +1783,16 @@ var package_default = {
|
|
|
1742
1783
|
bin: {
|
|
1743
1784
|
"mermaidjs-lint": "dist/bin.js"
|
|
1744
1785
|
},
|
|
1786
|
+
types: "./dist/types/index.d.ts",
|
|
1745
1787
|
exports: {
|
|
1746
1788
|
".": {
|
|
1789
|
+
types: "./dist/types/index.d.ts",
|
|
1747
1790
|
bun: "./src/index.ts",
|
|
1748
|
-
types: "./src/index.ts",
|
|
1749
1791
|
default: "./dist/index.js"
|
|
1750
1792
|
},
|
|
1751
1793
|
"./testing": {
|
|
1794
|
+
types: "./dist/types/testing.d.ts",
|
|
1752
1795
|
bun: "./src/testing.ts",
|
|
1753
|
-
types: "./src/testing.ts",
|
|
1754
1796
|
default: "./dist/testing.js"
|
|
1755
1797
|
}
|
|
1756
1798
|
},
|
|
@@ -1764,6 +1806,7 @@ var package_default = {
|
|
|
1764
1806
|
],
|
|
1765
1807
|
dependencies: {
|
|
1766
1808
|
"@mermaid-js/parser": "^1.1.0",
|
|
1809
|
+
"@types/mdast": "^4",
|
|
1767
1810
|
"colorjs.io": "^0.6.1",
|
|
1768
1811
|
"happy-dom": "^20.9.0",
|
|
1769
1812
|
"mdast-util-from-markdown": "^2.0.3",
|
|
@@ -1777,7 +1820,6 @@ var package_default = {
|
|
|
1777
1820
|
"@biomejs/biome": "^2",
|
|
1778
1821
|
"@jpeakai/prose-gates": "^0.2.0",
|
|
1779
1822
|
"@types/bun": "latest",
|
|
1780
|
-
"@types/mdast": "^4",
|
|
1781
1823
|
typescript: "^5"
|
|
1782
1824
|
}
|
|
1783
1825
|
};
|
|
@@ -1788,9 +1830,17 @@ import { extname, join as join2, resolve as resolve2 } from "node:path";
|
|
|
1788
1830
|
var DIAGRAM_FILE_EXTS = new Set([...DIAGRAM_EXTS, ".md", ".markdown"]);
|
|
1789
1831
|
var SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tmp", "__pycache__"]);
|
|
1790
1832
|
var skipDir = (name) => name.startsWith(".") || SKIP_DIRS.has(name);
|
|
1833
|
+
var linksToDir = (path) => {
|
|
1834
|
+
try {
|
|
1835
|
+
return statSync(path).isDirectory();
|
|
1836
|
+
} catch {
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
};
|
|
1791
1840
|
var collectFiles = (paths) => {
|
|
1792
1841
|
const files = [];
|
|
1793
1842
|
const unreadable = [];
|
|
1843
|
+
const symlinks = [];
|
|
1794
1844
|
const seen = new Set;
|
|
1795
1845
|
const walk = (dir) => {
|
|
1796
1846
|
let entries;
|
|
@@ -1802,7 +1852,11 @@ var collectFiles = (paths) => {
|
|
|
1802
1852
|
}
|
|
1803
1853
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
1804
1854
|
const abs = join2(dir, entry.name);
|
|
1805
|
-
if (entry.
|
|
1855
|
+
if (entry.isSymbolicLink()) {
|
|
1856
|
+
if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
|
|
1857
|
+
symlinks.push(abs);
|
|
1858
|
+
}
|
|
1859
|
+
} else if (entry.isDirectory()) {
|
|
1806
1860
|
if (!skipDir(entry.name))
|
|
1807
1861
|
walk(abs);
|
|
1808
1862
|
} else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
|
|
@@ -1827,7 +1881,7 @@ var collectFiles = (paths) => {
|
|
|
1827
1881
|
unreadable.push(p);
|
|
1828
1882
|
}
|
|
1829
1883
|
}
|
|
1830
|
-
return { files, unreadable };
|
|
1884
|
+
return { files, unreadable, symlinks };
|
|
1831
1885
|
};
|
|
1832
1886
|
|
|
1833
1887
|
// src/fix/verify.ts
|
|
@@ -1878,13 +1932,17 @@ var sameDiagrams = async (before, after) => {
|
|
|
1878
1932
|
};
|
|
1879
1933
|
var verify = async (edit, before, after) => {
|
|
1880
1934
|
if (edit.expect.kind === "same-tree") {
|
|
1881
|
-
|
|
1935
|
+
if (!sameTree(before.tree, after.tree, {}))
|
|
1936
|
+
return "refuse";
|
|
1937
|
+
if (before.kind === "markdown" && after.kind === "markdown")
|
|
1938
|
+
return "accept";
|
|
1882
1939
|
}
|
|
1883
1940
|
return await sameDiagrams(before, after) ? "accept" : "refuse";
|
|
1884
1941
|
};
|
|
1885
1942
|
|
|
1886
1943
|
// src/fix/engine.ts
|
|
1887
1944
|
var MAX_PASSES = 1000;
|
|
1945
|
+
var RECENT_PASSES = 50;
|
|
1888
1946
|
var splice = (src, edits) => {
|
|
1889
1947
|
let out = src;
|
|
1890
1948
|
for (const e of [...edits].sort((a, b) => b.start - a.start))
|
|
@@ -1902,6 +1960,7 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
|
1902
1960
|
const refused = [];
|
|
1903
1961
|
const refusedKeys = new Set;
|
|
1904
1962
|
let current = src;
|
|
1963
|
+
const keptBy = [];
|
|
1905
1964
|
for (let pass = 0;pass < MAX_PASSES; pass++) {
|
|
1906
1965
|
const doc = buildDocModel(current, kind);
|
|
1907
1966
|
const keyOf = (e) => `${e.rule}\x00${current.slice(e.start, e.end)}\x00${e.text}`;
|
|
@@ -1934,23 +1993,26 @@ var fixSourceReport = async (src, file, flags = {}, registry = BUILTIN) => {
|
|
|
1934
1993
|
continue;
|
|
1935
1994
|
if (edits.length > 1 && !overlaps(edits) && await attempt(edits)) {
|
|
1936
1995
|
progressed = true;
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1996
|
+
} else {
|
|
1997
|
+
for (const edit of edits) {
|
|
1998
|
+
if (await attempt([edit])) {
|
|
1999
|
+
progressed = true;
|
|
2000
|
+
break;
|
|
2001
|
+
}
|
|
2002
|
+
refusedKeys.add(keyOf(edit));
|
|
2003
|
+
refused.push(edit);
|
|
1943
2004
|
}
|
|
1944
|
-
refusedKeys.add(keyOf(edit));
|
|
1945
|
-
refused.push(edit);
|
|
1946
2005
|
}
|
|
1947
|
-
if (progressed)
|
|
2006
|
+
if (progressed) {
|
|
2007
|
+
keptBy.push(rule.id);
|
|
1948
2008
|
break;
|
|
2009
|
+
}
|
|
1949
2010
|
}
|
|
1950
2011
|
if (!progressed)
|
|
1951
2012
|
return { output: current, applied, refused };
|
|
1952
2013
|
}
|
|
1953
|
-
|
|
2014
|
+
const lingering = [...new Set(keptBy.slice(-RECENT_PASSES))].sort();
|
|
2015
|
+
throw new FixNotConvergingError(file, MAX_PASSES, lingering);
|
|
1954
2016
|
};
|
|
1955
2017
|
var fixSource = async (src, file, flags = {}, registry = BUILTIN) => (await fixSourceReport(src, file, flags, registry)).output;
|
|
1956
2018
|
|
|
@@ -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) {
|
|
@@ -2701,9 +2820,12 @@ export {
|
|
|
2701
2820
|
PluginFixerError,
|
|
2702
2821
|
PluginContractError,
|
|
2703
2822
|
PluginConflictError,
|
|
2823
|
+
PackageJsonError,
|
|
2704
2824
|
PRESETS,
|
|
2705
2825
|
MermaidLintError,
|
|
2706
2826
|
FlagError,
|
|
2827
|
+
FixOutsideRootError,
|
|
2828
|
+
FixNotConvergingError,
|
|
2707
2829
|
FIX_ORDER,
|
|
2708
2830
|
DIAGRAM_EXTS,
|
|
2709
2831
|
ConfigShapeError,
|