@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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Awaitable, CheckContext, Edit, Finding, FixContext, OptionSpec } from "../rules/types.ts";
|
|
2
|
+
export declare const API_VERSION = 1;
|
|
3
|
+
export type PluginFinding = Pick<Finding, "line" | "message"> & Partial<Pick<Finding, "endLine" | "remediation">>;
|
|
4
|
+
export type PluginEdit = Omit<Edit, "rule">;
|
|
5
|
+
export interface PluginRule {
|
|
6
|
+
category: string;
|
|
7
|
+
summary: string;
|
|
8
|
+
options?: OptionSpec;
|
|
9
|
+
check: (ctx: CheckContext) => Awaitable<PluginFinding[]>;
|
|
10
|
+
fix?: (ctx: FixContext) => Awaitable<PluginEdit[]>;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginMeta {
|
|
13
|
+
name: string;
|
|
14
|
+
namespace: string;
|
|
15
|
+
apiVersion: number;
|
|
16
|
+
categories?: Record<string, string>;
|
|
17
|
+
}
|
|
18
|
+
export interface Plugin {
|
|
19
|
+
meta: PluginMeta;
|
|
20
|
+
rules: Record<string, PluginRule>;
|
|
21
|
+
}
|
|
22
|
+
export declare const CATEGORY_WORD: RegExp;
|
|
23
|
+
export declare const NAMESPACE: RegExp;
|
|
24
|
+
export declare const RULE_KEY: RegExp;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Config } from "../config.ts";
|
|
2
|
+
import { PluginLoadError } from "../errors.ts";
|
|
3
|
+
export type SourceKind = "local" | "package" | "config";
|
|
4
|
+
export interface PluginSource {
|
|
5
|
+
kind: SourceKind;
|
|
6
|
+
spec: string;
|
|
7
|
+
path: string;
|
|
8
|
+
}
|
|
9
|
+
export interface UnresolvedSource {
|
|
10
|
+
kind: SourceKind;
|
|
11
|
+
error: PluginLoadError;
|
|
12
|
+
}
|
|
13
|
+
export interface Discovery {
|
|
14
|
+
sources: PluginSource[];
|
|
15
|
+
failures: UnresolvedSource[];
|
|
16
|
+
}
|
|
17
|
+
export declare const LOCAL_RULES: string;
|
|
18
|
+
export declare const PACKAGE_PATTERN: RegExp;
|
|
19
|
+
export declare const resolvePackage: (root: string, name: string) => Promise<string>;
|
|
20
|
+
export declare const discoverSources: (root: string, config: Config, pluginsOff?: boolean) => Promise<Discovery>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Config } from "../config.ts";
|
|
2
|
+
import { PluginLoadError } from "../errors.ts";
|
|
3
|
+
import { type LoadedSource } from "../rules/registry.ts";
|
|
4
|
+
import type { Rule } from "../rules/types.ts";
|
|
5
|
+
import { type SourceKind } from "./discover.ts";
|
|
6
|
+
export type PluginMode = "off" | "strict" | "lenient" | "collect";
|
|
7
|
+
export interface PluginFailure {
|
|
8
|
+
source: string;
|
|
9
|
+
kind: SourceKind;
|
|
10
|
+
error: PluginLoadError;
|
|
11
|
+
}
|
|
12
|
+
export interface PluginSet {
|
|
13
|
+
rules: Rule[];
|
|
14
|
+
categories: Map<string, string>;
|
|
15
|
+
sources: LoadedSource[];
|
|
16
|
+
skipped: string[];
|
|
17
|
+
failures: PluginFailure[];
|
|
18
|
+
}
|
|
19
|
+
export declare const loadPlugins: (root: string, config: Config, mode: PluginMode) => Promise<PluginSet>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PluginLoadError } from "../errors.ts";
|
|
2
|
+
import type { Rule } from "../rules/types.ts";
|
|
3
|
+
import { type Plugin, type PluginRule } from "./contract.ts";
|
|
4
|
+
import type { PluginSource } from "./discover.ts";
|
|
5
|
+
import { type RuntimeInfo } from "./runtime.ts";
|
|
6
|
+
export { PluginLoadError };
|
|
7
|
+
export declare const parsePlugin: (spec: string, raw: unknown, reserved: ReadonlySet<string>) => Plugin;
|
|
8
|
+
export declare const loadPackage: (source: PluginSource, reserved: ReadonlySet<string>) => Promise<Plugin>;
|
|
9
|
+
export interface LocalLoad {
|
|
10
|
+
plugin: Plugin | null;
|
|
11
|
+
failures: PluginLoadError[];
|
|
12
|
+
}
|
|
13
|
+
export declare const loadLocal: (sources: PluginSource[], runtime?: RuntimeInfo) => Promise<LocalLoad>;
|
|
14
|
+
export declare const toRule: (namespace: string, key: string, rule: PluginRule) => Rule;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface RuntimeInfo {
|
|
2
|
+
versions: Record<string, string | undefined>;
|
|
3
|
+
features: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export declare const canImportTypeScript: (runtime?: RuntimeInfo) => boolean;
|
|
6
|
+
export declare const TYPESCRIPT_HELP = "TypeScript cannot be imported by this runtime. Run with Bun (bunx --bun @jpeakai/mermaidjs-lint), use Node 22.18 or newer, or write a .js or .mjs file";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type PluginFailure } from "./plugins/index.ts";
|
|
2
|
+
import { type Registry } from "./rules/registry.ts";
|
|
3
|
+
export declare const LOCAL_DIR = ".mermaidjs-lint";
|
|
4
|
+
export declare const findProjectRoot: (cwd: string) => Promise<string>;
|
|
5
|
+
export interface SetupOptions {
|
|
6
|
+
cwd: string;
|
|
7
|
+
configPath?: string;
|
|
8
|
+
noPlugins?: boolean;
|
|
9
|
+
lenient?: boolean;
|
|
10
|
+
collect?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface Setup {
|
|
13
|
+
root: string;
|
|
14
|
+
registry: Registry;
|
|
15
|
+
skipped: string[];
|
|
16
|
+
failures: PluginFailure[];
|
|
17
|
+
}
|
|
18
|
+
export declare const setUp: ({ cwd, configPath, noPlugins, lenient, collect, }: SetupOptions) => Promise<Setup>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const complexityNodeCountAcceptableExceeded: import("./types.ts").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const complexityNodeCountCognitiveLimit: import("./types.ts").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const complexityNodeCountHardLimit: import("./types.ts").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const complexityVisualScoreAcceptableExceeded: import("./types.ts").Rule;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const complexityVisualScoreCritical: import("./types.ts").Rule;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ThresholdConfig } from "../parse/metrics.ts";
|
|
2
|
+
import { type Measured } from "./shared.ts";
|
|
3
|
+
import type { Rule, RuleId } from "./types.ts";
|
|
4
|
+
export interface Hit {
|
|
5
|
+
message: string;
|
|
6
|
+
remediation: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const complexityRule: (spec: {
|
|
9
|
+
id: RuleId;
|
|
10
|
+
summary: string;
|
|
11
|
+
hit: (m: Measured, c: ThresholdConfig) => Hit | null;
|
|
12
|
+
}) => Rule;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Config } from "../config.ts";
|
|
2
|
+
import { type Helpers, type Rule, type RuleId, type RuleOptions } from "./types.ts";
|
|
3
|
+
export declare const BUILTIN_CATEGORIES: ReadonlyMap<string, string>;
|
|
4
|
+
export declare const HELPERS: Helpers;
|
|
5
|
+
export interface LoadedSource {
|
|
6
|
+
kind: "local" | "package" | "config";
|
|
7
|
+
spec: string;
|
|
8
|
+
namespace: string;
|
|
9
|
+
rules: RuleId[];
|
|
10
|
+
}
|
|
11
|
+
export interface Registry {
|
|
12
|
+
rules: Rule[];
|
|
13
|
+
fixOrder: Rule[];
|
|
14
|
+
enabled: ReadonlySet<RuleId>;
|
|
15
|
+
options: ReadonlyMap<RuleId, RuleOptions>;
|
|
16
|
+
categories: ReadonlyMap<string, string>;
|
|
17
|
+
sources: LoadedSource[];
|
|
18
|
+
ignoredSettings: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface RegistryInput {
|
|
21
|
+
rules: Rule[];
|
|
22
|
+
fixOrder: Rule[];
|
|
23
|
+
categories: ReadonlyMap<string, string>;
|
|
24
|
+
sources?: LoadedSource[];
|
|
25
|
+
config?: Config;
|
|
26
|
+
ignoreUnknownPluginIds?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare const buildRegistry: (input: RegistryInput) => Registry;
|
|
29
|
+
export declare const BUILTIN: Registry;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Diagram } from "../model.ts";
|
|
2
|
+
import { type ContrastResult, type Profile } from "../parse/contrast.ts";
|
|
3
|
+
import { type ComplexityMetrics, type ThresholdConfig } from "../parse/metrics.ts";
|
|
4
|
+
import type { DiagramStats } from "../parse/stats.ts";
|
|
5
|
+
import type { Finding, OptionSpec, RuleContext, RuleId } from "./types.ts";
|
|
6
|
+
export declare const COMPLEXITY_OPTIONS: OptionSpec;
|
|
7
|
+
export declare const CONTRAST_OPTIONS: OptionSpec;
|
|
8
|
+
export interface Measured {
|
|
9
|
+
stats: DiagramStats;
|
|
10
|
+
metrics: ComplexityMetrics;
|
|
11
|
+
}
|
|
12
|
+
export declare const measure: (diagram: Diagram, config: ThresholdConfig) => Promise<Measured | null>;
|
|
13
|
+
export declare const finding: (ctx: RuleContext, diagram: Diagram, rule: RuleId, message: string, remediation?: string) => Finding;
|
|
14
|
+
export declare const boundaryHint: (boundaries: string[]) => string;
|
|
15
|
+
export declare const splitTarget: (c: ThresholdConfig) => string;
|
|
16
|
+
export declare const profileOf: (ctx: RuleContext) => Profile;
|
|
17
|
+
export declare const contrastOf: (diagram: Diagram, profile: Profile) => ContrastResult;
|
|
18
|
+
export declare const fileLine: (diagram: Diagram, line: number) => number;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { DocModel } from "../model.ts";
|
|
2
|
+
import type { compositeOver, wcagRatio } from "../parse/color.ts";
|
|
3
|
+
import type { detectKeyword } from "../parse/stats.ts";
|
|
4
|
+
export declare const RULE: {
|
|
5
|
+
readonly UNPARSEABLE: "parse-diagram-unparseable";
|
|
6
|
+
readonly FENCE_UNTERMINATED: "parse-fence-unterminated";
|
|
7
|
+
readonly NODES_HARD: "complexity-node-count-hard-limit";
|
|
8
|
+
readonly NODES_COGNITIVE: "complexity-node-count-cognitive-limit";
|
|
9
|
+
readonly NODES_ACCEPTABLE: "complexity-node-count-acceptable-exceeded";
|
|
10
|
+
readonly VCS_CRITICAL: "complexity-visual-score-critical";
|
|
11
|
+
readonly VCS_ACCEPTABLE: "complexity-visual-score-acceptable-exceeded";
|
|
12
|
+
readonly NESTING: "complexity-subgraph-nesting-too-deep";
|
|
13
|
+
readonly TEXT: "contrast-text-below-aa";
|
|
14
|
+
readonly BORDER: "contrast-border-below-ui-minimum";
|
|
15
|
+
readonly UNAUDITABLE: "contrast-directive-unauditable";
|
|
16
|
+
};
|
|
17
|
+
export type CoreRuleId = (typeof RULE)[keyof typeof RULE];
|
|
18
|
+
export type Awaitable<T> = T | Promise<T>;
|
|
19
|
+
export type PluginRuleId = `${string}/${string}`;
|
|
20
|
+
export declare const isPluginRuleId: (id: string) => id is PluginRuleId;
|
|
21
|
+
export type RuleId = CoreRuleId | PluginRuleId;
|
|
22
|
+
export interface Finding {
|
|
23
|
+
file: string;
|
|
24
|
+
line: number;
|
|
25
|
+
endLine: number;
|
|
26
|
+
rule: RuleId;
|
|
27
|
+
message: string;
|
|
28
|
+
remediation?: string;
|
|
29
|
+
}
|
|
30
|
+
export type OptionType = "number" | "string" | "boolean" | "string[]";
|
|
31
|
+
export type OptionSpec = Readonly<Record<string, OptionType>>;
|
|
32
|
+
export type RuleOptions = Readonly<Record<string, unknown>>;
|
|
33
|
+
export interface Helpers {
|
|
34
|
+
detectKeyword: typeof detectKeyword;
|
|
35
|
+
wcagRatio: typeof wcagRatio;
|
|
36
|
+
compositeOver: typeof compositeOver;
|
|
37
|
+
}
|
|
38
|
+
export interface RuleContext {
|
|
39
|
+
doc: DocModel;
|
|
40
|
+
file: string;
|
|
41
|
+
options: RuleOptions;
|
|
42
|
+
helpers: Helpers;
|
|
43
|
+
}
|
|
44
|
+
export type CheckContext = RuleContext;
|
|
45
|
+
export type FixContext = RuleContext;
|
|
46
|
+
export type Expectation = {
|
|
47
|
+
kind: "same-diagrams";
|
|
48
|
+
} | {
|
|
49
|
+
kind: "same-tree";
|
|
50
|
+
};
|
|
51
|
+
export interface Edit {
|
|
52
|
+
rule: RuleId;
|
|
53
|
+
start: number;
|
|
54
|
+
end: number;
|
|
55
|
+
text: string;
|
|
56
|
+
expect: Expectation;
|
|
57
|
+
}
|
|
58
|
+
export declare const CATEGORIES: readonly ["parse", "complexity", "contrast"];
|
|
59
|
+
export type Category = (typeof CATEGORIES)[number];
|
|
60
|
+
export interface Rule {
|
|
61
|
+
id: RuleId;
|
|
62
|
+
category: string;
|
|
63
|
+
summary: string;
|
|
64
|
+
options?: OptionSpec;
|
|
65
|
+
check: (ctx: CheckContext) => Awaitable<Finding[]>;
|
|
66
|
+
fix?: (ctx: FixContext) => Awaitable<Edit[]>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type RunFlags } from "./check.ts";
|
|
2
|
+
import { type FixReport } from "./fix/engine.ts";
|
|
3
|
+
import type { Plugin, PluginRule } from "./plugins/contract.ts";
|
|
4
|
+
import type { Finding, RuleOptions } from "./rules/types.ts";
|
|
5
|
+
export interface RunOptions {
|
|
6
|
+
options?: RuleOptions;
|
|
7
|
+
flags?: RunFlags;
|
|
8
|
+
file?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const localPlugin: (key: string, mod: {
|
|
11
|
+
default: PluginRule;
|
|
12
|
+
categories?: Record<string, string>;
|
|
13
|
+
}) => Plugin;
|
|
14
|
+
export declare const checkRule: (plugin: Plugin, key: string, src: string, run?: RunOptions) => Promise<Finding[]>;
|
|
15
|
+
export declare const fixRule: (plugin: Plugin, key: string, src: string, run?: RunOptions) => Promise<FixReport>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jpeakai/mermaidjs-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic lint gates and verified fixers for Mermaid diagrams in markdown",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,15 +29,16 @@
|
|
|
29
29
|
"bin": {
|
|
30
30
|
"mermaidjs-lint": "dist/bin.js"
|
|
31
31
|
},
|
|
32
|
+
"types": "./dist/types/index.d.ts",
|
|
32
33
|
"exports": {
|
|
33
34
|
".": {
|
|
35
|
+
"types": "./dist/types/index.d.ts",
|
|
34
36
|
"bun": "./src/index.ts",
|
|
35
|
-
"types": "./src/index.ts",
|
|
36
37
|
"default": "./dist/index.js"
|
|
37
38
|
},
|
|
38
39
|
"./testing": {
|
|
40
|
+
"types": "./dist/types/testing.d.ts",
|
|
39
41
|
"bun": "./src/testing.ts",
|
|
40
|
-
"types": "./src/testing.ts",
|
|
41
42
|
"default": "./dist/testing.js"
|
|
42
43
|
}
|
|
43
44
|
},
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
],
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"@mermaid-js/parser": "^1.1.0",
|
|
55
|
+
"@types/mdast": "^4",
|
|
54
56
|
"colorjs.io": "^0.6.1",
|
|
55
57
|
"happy-dom": "^20.9.0",
|
|
56
58
|
"mdast-util-from-markdown": "^2.0.3",
|
|
@@ -64,7 +66,6 @@
|
|
|
64
66
|
"@biomejs/biome": "^2",
|
|
65
67
|
"@jpeakai/prose-gates": "^0.2.0",
|
|
66
68
|
"@types/bun": "latest",
|
|
67
|
-
"@types/mdast": "^4",
|
|
68
69
|
"typescript": "^5"
|
|
69
70
|
}
|
|
70
71
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// The command line: check files, optionally fix them first. The process glue
|
|
2
2
|
// lives in bin.ts so tests can call main() directly.
|
|
3
3
|
|
|
4
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
import { relative } from "node:path";
|
|
4
|
+
import { lstat, readFile, realpath, writeFile } from "node:fs/promises";
|
|
5
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
6
6
|
import { parseArgs } from "node:util";
|
|
7
7
|
import pkg from "../package.json" with { type: "json" };
|
|
8
8
|
import { checkModel, type RunFlags } from "./check.ts";
|
|
9
|
-
import { FlagError, MermaidLintError, UsageError } from "./errors.ts";
|
|
9
|
+
import { FixOutsideRootError, FlagError, MermaidLintError, UsageError } from "./errors.ts";
|
|
10
10
|
import { collectFiles, DIAGRAM_FILE_EXTS, SKIP_DIRS } from "./files.ts";
|
|
11
11
|
import { fixSourceReport } from "./fix/engine.ts";
|
|
12
12
|
import { buildDocModel, DIAGRAM_EXTS, documentKind } from "./model.ts";
|
|
@@ -16,7 +16,7 @@ import type { PluginFailure } from "./plugins/index.ts";
|
|
|
16
16
|
import { setUp } from "./project.ts";
|
|
17
17
|
import { RULES } from "./rules/index.ts";
|
|
18
18
|
import type { LoadedSource, Registry } from "./rules/registry.ts";
|
|
19
|
-
import { CATEGORIES, type Finding } from "./rules/types.ts";
|
|
19
|
+
import { CATEGORIES, type Finding, type Rule } from "./rules/types.ts";
|
|
20
20
|
|
|
21
21
|
// The invocation forms, printed alone after a usage error so the error is not
|
|
22
22
|
// buried, and first in --help.
|
|
@@ -26,19 +26,25 @@ const SYNOPSIS = `usage: mermaidjs-lint <path...> [--fix] [--json] [--preset NAM
|
|
|
26
26
|
mermaidjs-lint --validate-plugins [--json] [--config FILE]
|
|
27
27
|
mermaidjs-lint --version | -h, --help`;
|
|
28
28
|
|
|
29
|
+
// One rule as help and --list-rules print it: the id padded to a column, the
|
|
30
|
+
// autofix marker inside it, then the summary, so every summary lines up.
|
|
31
|
+
const ruleRow = (r: Rule, width: number): string => `${`${r.id}${r.fix ? "*" : ""}`.padEnd(width)} ${r.summary}`;
|
|
32
|
+
|
|
33
|
+
const idWidth = (rules: Rule[]): number => Math.max(0, ...rules.map((r) => r.id.length + 1));
|
|
34
|
+
|
|
29
35
|
const USAGE = `${SYNOPSIS}
|
|
30
36
|
|
|
31
37
|
Deterministic Mermaid diagram gates (* = autofixable):
|
|
32
38
|
${CATEGORIES.map((c) =>
|
|
33
|
-
[` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${r
|
|
34
|
-
"\n",
|
|
35
|
-
),
|
|
39
|
+
[` ${c}:`, ...RULES.filter((r) => r.category === c).map((r) => ` ${ruleRow(r, idWidth(RULES))}`)].join("\n"),
|
|
36
40
|
).join("\n")}
|
|
37
41
|
|
|
38
42
|
A path is a file or a directory, and every path must exist. A file ending in
|
|
39
43
|
${DIAGRAM_EXTS.join(" or ")} is read as one raw diagram, and any other file is read as markdown
|
|
40
44
|
whose mermaid fences are checked. A directory is walked for ${[...DIAGRAM_FILE_EXTS].join(", ")} files,
|
|
41
|
-
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}.
|
|
45
|
+
skipping dot-folders and ${[...SKIP_DIRS].join(", ")}. The walk does not follow a symbolic link,
|
|
46
|
+
and names each one it skipped. A link named as a path is read, but --fix does not write
|
|
47
|
+
through one whose target is outside the project root.
|
|
42
48
|
Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lint/reference/rules/
|
|
43
49
|
|
|
44
50
|
-p, --preset NAME sets the density for every complexity rule: ${PRESET_NAMES.join(", ")}
|
|
@@ -49,14 +55,16 @@ Examples of every rule, with fixes: https://jpeakai.github.io/jpai-mermaidjs-lin
|
|
|
49
55
|
--fix applies every fix it can prove safe, then reports what remains.
|
|
50
56
|
A fixer that is unsure leaves the text alone and the finding stays.
|
|
51
57
|
--config FILE reads that config instead of mermaidjs-lint.config.json or .mjs at the project root.
|
|
52
|
-
Plugins load on their own from .mermaidjs-lint/rules
|
|
58
|
+
Plugins load on their own from .mermaidjs-lint/rules, from dependencies named mermaidjs-lint-plugin,
|
|
59
|
+
mermaidjs-lint-plugin-* or @scope/mermaidjs-lint-plugin(-*), and from the config's plugins list.
|
|
53
60
|
--no-plugins runs the built-in rules only and loads no plugin. It reads a .json config as usual,
|
|
54
61
|
and stops with a usage error rather than import a .mjs config, which is code.
|
|
55
62
|
--lenient-plugins skips a plugin that fails to load and says so, instead of stopping the run.
|
|
56
|
-
--validate-plugins
|
|
63
|
+
--validate-plugins imports and checks every plugin, reports every failure, and runs no check or fix.
|
|
57
64
|
--list-rules prints every active rule by category, including plugin rules, and where each plugin came from.
|
|
65
|
+
It imports the plugins to list them.
|
|
58
66
|
--version prints the version, to quote in a bug report.
|
|
59
|
-
Exits 0 when no findings remain, 1 when findings remain
|
|
67
|
+
Exits 0 when no findings remain, 1 when findings remain, a plugin fails to load or the run fails (a lenient run
|
|
60
68
|
skips a broken plugin, so it alone does not exit 1), and 2 on a usage error.`;
|
|
61
69
|
|
|
62
70
|
interface Parsed {
|
|
@@ -122,11 +130,12 @@ export const skippedLines = (failures: PluginFailure[], ignored: string[]): stri
|
|
|
122
130
|
// Every active rule under its category, then where each plugin came from.
|
|
123
131
|
export const listRules = (registry: Registry): string => {
|
|
124
132
|
const lines: string[] = [];
|
|
133
|
+
const width = idWidth(registry.rules);
|
|
125
134
|
for (const [category, description] of registry.categories) {
|
|
126
135
|
const rules = registry.rules.filter((r) => r.category === category);
|
|
127
136
|
if (rules.length === 0) continue;
|
|
128
137
|
lines.push(`${category}: ${description}`);
|
|
129
|
-
for (const r of rules) lines.push(` ${r
|
|
138
|
+
for (const r of rules) lines.push(` ${ruleRow(r, width)}`);
|
|
130
139
|
}
|
|
131
140
|
if (registry.sources.length > 0) {
|
|
132
141
|
lines.push("", "plugins:");
|
|
@@ -180,6 +189,19 @@ const display = (file: string, cwd: string): string => {
|
|
|
180
189
|
return !rel || rel.startsWith("..") ? file : rel;
|
|
181
190
|
};
|
|
182
191
|
|
|
192
|
+
// --fix writes through a symbolic link to its target, so a link whose target
|
|
193
|
+
// sits outside the project root would let a run edit a file in another tree.
|
|
194
|
+
// Every such link is refused before any file is written.
|
|
195
|
+
const refuseOutsideLinks = async (files: string[], root: string, cwd: string): Promise<void> => {
|
|
196
|
+
const realRoot = await realpath(root);
|
|
197
|
+
for (const file of files) {
|
|
198
|
+
if (!(await lstat(file)).isSymbolicLink()) continue;
|
|
199
|
+
const target = await realpath(file);
|
|
200
|
+
const rel = relative(realRoot, target);
|
|
201
|
+
if (rel.startsWith("..") || isAbsolute(rel)) throw new FixOutsideRootError(display(file, cwd), target, root);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
183
205
|
const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
184
206
|
let parsed: Parsed;
|
|
185
207
|
try {
|
|
@@ -215,7 +237,7 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
215
237
|
}
|
|
216
238
|
if (positionals.length === 0 && !listing && !validating) throw new FlagError("files", "no files given");
|
|
217
239
|
|
|
218
|
-
const { registry, skipped, failures } = await setUp({
|
|
240
|
+
const { root, registry, skipped, failures } = await setUp({
|
|
219
241
|
cwd,
|
|
220
242
|
configPath: values.config,
|
|
221
243
|
noPlugins: values["no-plugins"],
|
|
@@ -238,10 +260,18 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
238
260
|
return 0;
|
|
239
261
|
}
|
|
240
262
|
|
|
241
|
-
const { files, unreadable } = collectFiles(positionals.map((p) => (
|
|
263
|
+
const { files, unreadable, symlinks } = collectFiles(positionals.map((p) => resolve(cwd, p)));
|
|
242
264
|
if (unreadable.length > 0) throw new FlagError("files", `cannot read ${unreadable.join(", ")}`);
|
|
265
|
+
for (const link of symlinks) {
|
|
266
|
+
console.error(`mermaidjs-lint: skipped symbolic link ${display(link, cwd)}; name it as a path to check it`);
|
|
267
|
+
}
|
|
243
268
|
if (files.length === 0) throw new FlagError("files", "no matching files");
|
|
269
|
+
if (values.fix) await refuseOutsideLinks(files, root, cwd);
|
|
244
270
|
|
|
271
|
+
// Every rule, in check and in fix, sees the file as an absolute path, so a
|
|
272
|
+
// rule that reads the path sees one form, and the auto profile is found
|
|
273
|
+
// from the file's own folder whatever the process's working directory. The
|
|
274
|
+
// report shows the path relative to cwd.
|
|
245
275
|
const flags: RunFlags = { preset: values.preset, profile: values.profile };
|
|
246
276
|
const perFile = await Promise.all(
|
|
247
277
|
files.map(async (file) => {
|
|
@@ -253,7 +283,9 @@ const run = async (argv: string[], cwd: string): Promise<number> => {
|
|
|
253
283
|
src = fixed;
|
|
254
284
|
}
|
|
255
285
|
}
|
|
256
|
-
|
|
286
|
+
const shown = display(file, cwd);
|
|
287
|
+
const found = await checkModel(buildDocModel(src, documentKind(file)), file, flags, registry);
|
|
288
|
+
return found.map((f) => ({ ...f, file: shown }));
|
|
257
289
|
}),
|
|
258
290
|
);
|
|
259
291
|
const all = perFile.flat();
|
package/src/errors.ts
CHANGED
|
@@ -130,6 +130,52 @@ export class RuleOptionError extends UsageError {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// --fix would write through a symbolic link whose target sits outside the
|
|
134
|
+
// project root, so the run stops before any file is written.
|
|
135
|
+
export class FixOutsideRootError extends UsageError {
|
|
136
|
+
readonly file: string;
|
|
137
|
+
readonly target: string;
|
|
138
|
+
|
|
139
|
+
constructor(file: string, target: string, root: string) {
|
|
140
|
+
super(
|
|
141
|
+
`${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`,
|
|
142
|
+
"fix-outside-root",
|
|
143
|
+
);
|
|
144
|
+
this.file = file;
|
|
145
|
+
this.target = target;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- runtime: the project or a fixer is in a state the run cannot use -------
|
|
150
|
+
|
|
151
|
+
// A package.json the run reads, the project's own or an installed plugin's, is
|
|
152
|
+
// not a JSON object. Exits 1, like any runtime failure.
|
|
153
|
+
export class PackageJsonError extends MermaidLintError {
|
|
154
|
+
readonly file: string;
|
|
155
|
+
|
|
156
|
+
constructor(file: string) {
|
|
157
|
+
super("package-json", `${file} is not a JSON object; fix or remove it`);
|
|
158
|
+
this.file = file;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A fixer, built-in or plugin, kept proposing edits that verified, so the fix
|
|
163
|
+
// never reached a fixpoint. A fixer must be idempotent: once its edit is
|
|
164
|
+
// applied, it proposes nothing for the same text. Nothing is written.
|
|
165
|
+
export class FixNotConvergingError extends MermaidLintError {
|
|
166
|
+
readonly file: string;
|
|
167
|
+
readonly rules: string[];
|
|
168
|
+
|
|
169
|
+
constructor(file: string, passes: number, rules: string[]) {
|
|
170
|
+
super(
|
|
171
|
+
"fix-not-converging",
|
|
172
|
+
`${file}: --fix did not converge after ${passes} passes; ${rules.join(", ")} kept proposing edits. A fixer must propose nothing once its edit is applied`,
|
|
173
|
+
);
|
|
174
|
+
this.file = file;
|
|
175
|
+
this.rules = rules;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
133
179
|
// --- plugins: a source could not be loaded --------------------------------
|
|
134
180
|
|
|
135
181
|
// A source that failed to load. `source` is what the user would recognise, a
|
package/src/files.ts
CHANGED
|
@@ -17,15 +17,30 @@ export const SKIP_DIRS = new Set(["node_modules", "vendor", "dist", "build", "tm
|
|
|
17
17
|
|
|
18
18
|
const skipDir = (name: string): boolean => name.startsWith(".") || SKIP_DIRS.has(name);
|
|
19
19
|
|
|
20
|
+
// Whether a link resolves to a directory. A dangling link resolves to nothing.
|
|
21
|
+
const linksToDir = (path: string): boolean => {
|
|
22
|
+
try {
|
|
23
|
+
return statSync(path).isDirectory();
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
20
29
|
export interface Collected {
|
|
21
30
|
files: string[];
|
|
22
31
|
// Paths that could not be read, which the caller reports as usage errors.
|
|
23
32
|
unreadable: string[];
|
|
33
|
+
// Symbolic links a directory walk passed over, to files or folders alike.
|
|
34
|
+
// A walk never follows a link, so it cannot leave the tree it was given or
|
|
35
|
+
// loop, and the caller says which it skipped so a gate never passes in
|
|
36
|
+
// silence over a file it did not read. A link named as a path is followed.
|
|
37
|
+
symlinks: string[];
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
export const collectFiles = (paths: string[]): Collected => {
|
|
27
41
|
const files: string[] = [];
|
|
28
42
|
const unreadable: string[] = [];
|
|
43
|
+
const symlinks: string[] = [];
|
|
29
44
|
const seen = new Set<string>();
|
|
30
45
|
|
|
31
46
|
const walk = (dir: string): void => {
|
|
@@ -38,7 +53,11 @@ export const collectFiles = (paths: string[]): Collected => {
|
|
|
38
53
|
}
|
|
39
54
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
40
55
|
const abs = join(dir, entry.name);
|
|
41
|
-
if (entry.
|
|
56
|
+
if (entry.isSymbolicLink()) {
|
|
57
|
+
if (!skipDir(entry.name) && (DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase()) || linksToDir(abs))) {
|
|
58
|
+
symlinks.push(abs);
|
|
59
|
+
}
|
|
60
|
+
} else if (entry.isDirectory()) {
|
|
42
61
|
if (!skipDir(entry.name)) walk(abs);
|
|
43
62
|
} else if (entry.isFile() && DIAGRAM_FILE_EXTS.has(extname(entry.name).toLowerCase())) {
|
|
44
63
|
if (!seen.has(abs)) {
|
|
@@ -64,5 +83,5 @@ export const collectFiles = (paths: string[]): Collected => {
|
|
|
64
83
|
unreadable.push(p);
|
|
65
84
|
}
|
|
66
85
|
}
|
|
67
|
-
return { files, unreadable };
|
|
86
|
+
return { files, unreadable, symlinks };
|
|
68
87
|
};
|