dslinter 0.1.13 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +50 -29
- package/bin/dslinter.mjs +26 -5
- package/bin/lib/config-hide-component.mjs +44 -0
- package/bin/lib/config-hide-component.test.mjs +33 -0
- package/bin/lib/constants.mjs +20 -0
- package/bin/lib/dev-banner.mjs +16 -51
- package/bin/lib/dev-banner.test.mjs +20 -18
- package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
- package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
- package/bin/lib/enrich-report-cli.mjs +14 -0
- package/bin/lib/env.mjs +20 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
- package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
- package/bin/lib/parse-args.mjs +13 -1
- package/bin/lib/parse-args.test.mjs +7 -1
- package/bin/lib/paths.mjs +8 -0
- package/bin/lib/project-root.mjs +72 -10
- package/bin/lib/project-root.test.mjs +32 -1
- package/bin/lib/prompt.mjs +31 -0
- package/bin/lib/resolve-project.mjs +78 -0
- package/bin/lib/resolve-project.test.mjs +74 -0
- package/bin/lib/run-scanner.mjs +40 -6
- package/bin/lib/scaffold-config.mjs +96 -8
- package/bin/lib/scaffold-config.test.mjs +12 -2
- package/bin/lib/scan-host.mjs +44 -0
- package/bin/lib/scan-host.test.mjs +41 -0
- package/bin/lib/setup-readiness.mjs +153 -0
- package/bin/lib/setup-readiness.test.mjs +32 -0
- package/bin/modes/build.mjs +31 -6
- package/bin/modes/dev.mjs +55 -21
- package/bin/modes/init.mjs +3 -22
- package/bin/modes/init.test.mjs +1 -1
- package/bin/modes/mcp.mjs +49 -0
- package/bin/modes/report.mjs +29 -4
- package/bin/modes/watch.mjs +85 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
- package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
- package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
- package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +53 -52
- package/index.d.ts +3 -0
- package/package.json +18 -12
- package/shared/env.ts +15 -0
- package/shared/paths.ts +8 -0
- package/shared/reportPath.test.ts +19 -0
- package/shared/reportPath.ts +12 -0
- package/shared/servePort.ts +16 -0
- package/src/components/ComponentInspectPane.tsx +67 -19
- package/src/components/ComponentPlaygroundPane.tsx +262 -113
- package/src/components/DashboardCommandPalette.tsx +6 -11
- package/src/components/GovernancePane.tsx +2 -2
- package/src/components/HideFromCatalogButton.tsx +44 -0
- package/src/components/OpenInEditorButton.tsx +36 -0
- package/src/components/PlaygroundA11yAndCode.tsx +53 -53
- package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
- package/src/components/PlaygroundControls.tsx +5 -11
- package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
- package/src/components/PlaygroundUsageCode.tsx +6 -4
- package/src/components/PlaygroundVariantMatrix.tsx +101 -34
- package/src/components/Section.tsx +5 -2
- package/src/components/Sidebar.tsx +131 -46
- package/src/components/TruncatedPath.tsx +44 -0
- package/src/components/controlApiTable.test.ts +29 -0
- package/src/components/controlApiTable.ts +3 -0
- package/src/components/playgroundUsageHighlight.ts +14 -3
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/table.tsx +2 -2
- package/src/dashboard/ComponentCatalog.tsx +16 -23
- package/src/dashboard/ComponentUsageDetails.tsx +6 -15
- package/src/dashboard/DashboardBody.tsx +0 -35
- package/src/dashboard/FindingsList.tsx +65 -55
- package/src/dashboard/ScannedTokenWall.tsx +3 -3
- package/src/dashboard/aggregate.test.ts +74 -0
- package/src/dashboard/aggregate.ts +145 -21
- package/src/dashboard/catalogVisibility.test.ts +93 -0
- package/src/dashboard/catalogVisibility.ts +108 -0
- package/src/dashboard/editorLink.test.ts +57 -0
- package/src/dashboard/editorLink.ts +71 -0
- package/src/dashboard/paths.test.ts +49 -0
- package/src/dashboard/paths.ts +51 -3
- package/src/dashboard/updateDslintConfig.ts +22 -0
- package/src/dashboard/useWorkspaceReport.ts +21 -17
- package/src/index.ts +26 -0
- package/src/mcp/agent-context.ts +148 -0
- package/src/mcp/agent-query.test.ts +89 -0
- package/src/mcp/agent-query.ts +373 -0
- package/src/mcp/config.ts +53 -0
- package/src/mcp/index.ts +18 -0
- package/src/mcp/normalize-paths.ts +65 -0
- package/src/mcp/report-cache.ts +209 -0
- package/src/mcp/rule-catalog.json +156 -0
- package/src/mcp/rule-catalog.ts +33 -0
- package/src/mcp/schemas.ts +54 -0
- package/src/mcp/server.test.ts +44 -0
- package/src/mcp/server.ts +343 -0
- package/src/mcp/start.ts +29 -0
- package/src/mcp/verify-loop.test.ts +49 -0
- package/src/mcp/verify-loop.ts +149 -0
- package/src/playground/appPreviewTheme.test.ts +148 -0
- package/src/playground/appPreviewTheme.ts +137 -0
- package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
- package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
- package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
- package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
- package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
- package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
- package/src/playground/collectDefinedPlaygrounds.ts +68 -0
- package/src/playground/controls.ts +177 -0
- package/src/playground/createPlaygroundRegistry.ts +1 -1
- package/src/playground/definePlayground.tsx +88 -16
- package/src/playground/definePlaygroundFromKit.ts +17 -0
- package/src/playground/embedGlobKey.ts +8 -0
- package/src/playground/enrichKitControls.test.ts +25 -0
- package/src/playground/enrichKitControls.ts +197 -0
- package/src/playground/expandPlaygroundControls.test.ts +50 -0
- package/src/playground/expandPlaygroundControls.ts +97 -0
- package/src/playground/inferKitJsx.test.ts +77 -0
- package/src/playground/inferKitJsx.ts +165 -0
- package/src/playground/inferKitParams.test.ts +41 -0
- package/src/playground/inferKitParams.ts +113 -0
- package/src/playground/inferPropTypesFromTs.d.mts +47 -0
- package/src/playground/inferPropTypesFromTs.mjs +343 -0
- package/src/playground/inferPropTypesFromTs.test.ts +227 -0
- package/src/playground/inferPropTypesFromTs.ts +17 -0
- package/src/playground/mergePlaygroundEntries.test.ts +32 -0
- package/src/playground/mergePlaygroundEntries.ts +28 -0
- package/src/playground/playgroundJoin.test.ts +79 -19
- package/src/playground/playgroundJoin.ts +47 -22
- package/src/playground/playgroundModuleExport.test.ts +42 -0
- package/src/playground/playgroundModuleExport.ts +22 -0
- package/src/playground/playgroundSpecsKey.ts +8 -0
- package/src/playground/propCoerce.ts +91 -0
- package/src/playground/scanVariantA11y.test.ts +46 -0
- package/src/playground/scanVariantA11y.ts +107 -0
- package/src/playground/snippet.ts +83 -0
- package/src/playground/usePlaygroundFromReport.test.ts +18 -8
- package/src/playground/usePlaygroundFromReport.ts +3 -1
- package/src/report/a11yForModule.ts +2 -7
- package/src/report/a11yScoring.test.ts +24 -0
- package/src/report/a11yScoring.ts +17 -0
- package/src/report/index.ts +6 -0
- package/src/shell/DashboardLayout.tsx +71 -45
- package/src/shell/DashboardLayoutAuto.tsx +0 -4
- package/src/shell/hashRoute.test.ts +7 -15
- package/src/shell/hashRoute.ts +31 -31
- package/src/shell/useHashRoute.ts +38 -13
- package/src/styles/dashboard-theme.css +18 -7
- package/src/types/controls.ts +11 -0
- package/src/types/playground.ts +4 -0
- package/src/types/report.ts +32 -9
- package/templates/playground/buildRegistry.ts +1 -1
- package/templates/vite.dslinter.snippet.ts +15 -4
- package/vite/collectScanModules.test.ts +51 -3
- package/vite/collectScanModules.ts +85 -29
- package/vite/consumer.config.mjs +6 -3
- package/vite/consumerAlias.test.ts +47 -0
- package/vite/consumerAlias.ts +114 -0
- package/vite/embedTailwindSources.test.ts +74 -0
- package/vite/embedTailwindSources.ts +97 -0
- package/vite/loadConsumerAliases.test.ts +131 -0
- package/vite/loadConsumerAliases.ts +155 -0
- package/vite/openFileInEditor.mjs +196 -0
- package/vite/openFileInEditor.test.mjs +87 -0
- package/vite/plugin.resolve.test.ts +72 -0
- package/vite/plugin.ts +216 -19
- package/vite/reportPath.test.ts +19 -0
- package/vite/resolveWayfinderImport.ts +56 -0
- package/vite/shims/inertia-react.tsx +85 -0
- package/vite/shims/wayfinder-actions.ts +33 -0
- package/vite/shims/wayfinder-routes.ts +30 -0
- package/vite/shims/ziggy-js.ts +12 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
- package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
- package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
- package/src/components/playgroundUsageTwoslash.ts +0 -69
- package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { enrichPlaygroundsFromTs } from "../../bin/lib/enrich-playgrounds-from-ts.mjs";
|
|
7
|
+
import { runScannerSync } from "../../bin/lib/run-scanner.mjs";
|
|
8
|
+
import type { McpConfig } from "./config";
|
|
9
|
+
import { normalizeReportPaths } from "./normalize-paths";
|
|
10
|
+
import type { WorkspaceReport } from "../types/report";
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
14
|
+
|
|
15
|
+
export type ReportCacheState = {
|
|
16
|
+
report: WorkspaceReport | null;
|
|
17
|
+
loadedAt: number;
|
|
18
|
+
source: "file" | "scan" | "dev";
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class ReportCache {
|
|
22
|
+
private state: ReportCacheState = {
|
|
23
|
+
report: null,
|
|
24
|
+
loadedAt: 0,
|
|
25
|
+
source: "file",
|
|
26
|
+
};
|
|
27
|
+
private sseAbort: AbortController | null = null;
|
|
28
|
+
private readonly config: McpConfig;
|
|
29
|
+
|
|
30
|
+
constructor(config: McpConfig) {
|
|
31
|
+
this.config = config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async getReport(opts: { fresh?: boolean } = {}): Promise<WorkspaceReport> {
|
|
35
|
+
if (!opts.fresh && this.state.report && this.isFresh()) {
|
|
36
|
+
return this.state.report;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const fromDev = await this.tryLoadDevServer();
|
|
40
|
+
if (fromDev) {
|
|
41
|
+
this.setState(fromDev, "dev");
|
|
42
|
+
return fromDev;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!opts.fresh && (await this.tryLoadFile())) {
|
|
46
|
+
return this.state.report!;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const scanned = await this.runScan();
|
|
50
|
+
this.setState(scanned, "scan");
|
|
51
|
+
return scanned;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
startDevWatch(): void {
|
|
55
|
+
if (this.sseAbort) return;
|
|
56
|
+
this.sseAbort = new AbortController();
|
|
57
|
+
void this.watchDevSse(this.sseAbort.signal);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
stopDevWatch(): void {
|
|
61
|
+
this.sseAbort?.abort();
|
|
62
|
+
this.sseAbort = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
invalidate(): void {
|
|
66
|
+
this.state.loadedAt = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
reportHash(): string | null {
|
|
70
|
+
if (!this.state.report) return null;
|
|
71
|
+
const json = JSON.stringify(this.state.report);
|
|
72
|
+
return createHash("sha256").update(json).digest("hex");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private isFresh(): boolean {
|
|
76
|
+
return Date.now() - this.state.loadedAt < this.config.ttlMs;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private setState(report: WorkspaceReport, source: ReportCacheState["source"]): void {
|
|
80
|
+
this.state = { report, loadedAt: Date.now(), source };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async tryLoadFile(): Promise<boolean> {
|
|
84
|
+
try {
|
|
85
|
+
const st = await stat(this.config.reportPath);
|
|
86
|
+
if (Date.now() - st.mtimeMs > this.config.ttlMs) return false;
|
|
87
|
+
const raw = await readFile(this.config.reportPath, "utf8");
|
|
88
|
+
const report = normalizeReportPaths(JSON.parse(raw) as WorkspaceReport);
|
|
89
|
+
this.setState(report, "file");
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async tryLoadDevServer(): Promise<WorkspaceReport | null> {
|
|
97
|
+
const url = `${this.config.devUrl.replace(/\/$/, "")}/dslinter-report.json`;
|
|
98
|
+
try {
|
|
99
|
+
const res = await fetch(url, {
|
|
100
|
+
cache: "no-store",
|
|
101
|
+
signal: AbortSignal.timeout(2000),
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) return null;
|
|
104
|
+
const report = normalizeReportPaths((await res.json()) as WorkspaceReport);
|
|
105
|
+
return report;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async watchDevSse(signal: AbortSignal): Promise<void> {
|
|
112
|
+
const url = `${this.config.devUrl.replace(/\/$/, "")}/events`;
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(url, { headers: { Accept: "text/event-stream" }, signal });
|
|
115
|
+
if (!res.ok || !res.body) return;
|
|
116
|
+
const reader = res.body.getReader();
|
|
117
|
+
const decoder = new TextDecoder();
|
|
118
|
+
let buffer = "";
|
|
119
|
+
while (!signal.aborted) {
|
|
120
|
+
const { done, value } = await reader.read();
|
|
121
|
+
if (done) break;
|
|
122
|
+
buffer += decoder.decode(value, { stream: true });
|
|
123
|
+
if (buffer.includes("data: updated")) {
|
|
124
|
+
buffer = "";
|
|
125
|
+
this.invalidate();
|
|
126
|
+
const fromDev = await this.tryLoadDevServer();
|
|
127
|
+
if (fromDev) this.setState(fromDev, "dev");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
// dev server not running
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async runScan(): Promise<WorkspaceReport> {
|
|
136
|
+
const args = [
|
|
137
|
+
this.config.scanPath,
|
|
138
|
+
"--output",
|
|
139
|
+
this.config.reportPath,
|
|
140
|
+
"--json",
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
let json = "";
|
|
144
|
+
try {
|
|
145
|
+
const { scanWorkspaceJson } = require(join(packageRoot, "index.cjs"));
|
|
146
|
+
json = scanWorkspaceJson(this.config.scanPath, false);
|
|
147
|
+
} catch {
|
|
148
|
+
const code = runScannerSync(args, {
|
|
149
|
+
projectRoot: this.config.projectRoot,
|
|
150
|
+
captureStdout: true,
|
|
151
|
+
});
|
|
152
|
+
if (code !== 0) {
|
|
153
|
+
throw new Error(`dslinter scan failed with exit code ${code}`);
|
|
154
|
+
}
|
|
155
|
+
json = await readFile(this.config.reportPath, "utf8");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let report = normalizeReportPaths(JSON.parse(json) as WorkspaceReport);
|
|
159
|
+
await enrichPlaygroundsFromTs({
|
|
160
|
+
projectRoot: this.config.projectRoot,
|
|
161
|
+
reportPath: this.config.reportPath,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const enriched = await readFile(this.config.reportPath, "utf8");
|
|
166
|
+
report = normalizeReportPaths(JSON.parse(enriched) as WorkspaceReport);
|
|
167
|
+
} catch {
|
|
168
|
+
// use scan-only report
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await mkdir(dirname(this.config.reportPath), { recursive: true });
|
|
172
|
+
await writeFile(this.config.reportPath, JSON.stringify(report, null, 2));
|
|
173
|
+
|
|
174
|
+
return report;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type BaselineStore = {
|
|
179
|
+
hash: string;
|
|
180
|
+
saved_at: string;
|
|
181
|
+
scores: WorkspaceReport["scores"];
|
|
182
|
+
finding_count: number;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export async function loadBaseline(projectRoot: string): Promise<BaselineStore | null> {
|
|
186
|
+
const path = join(projectRoot, ".dslinter", "mcp-baseline.json");
|
|
187
|
+
try {
|
|
188
|
+
const raw = await readFile(path, "utf8");
|
|
189
|
+
return JSON.parse(raw) as BaselineStore;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function saveBaseline(
|
|
196
|
+
projectRoot: string,
|
|
197
|
+
report: WorkspaceReport,
|
|
198
|
+
hash: string,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const dir = join(projectRoot, ".dslinter");
|
|
201
|
+
await mkdir(dir, { recursive: true });
|
|
202
|
+
const payload: BaselineStore = {
|
|
203
|
+
hash,
|
|
204
|
+
saved_at: new Date().toISOString(),
|
|
205
|
+
scores: report.scores,
|
|
206
|
+
finding_count: report.findings?.length ?? 0,
|
|
207
|
+
};
|
|
208
|
+
await writeFile(join(dir, "mcp-baseline.json"), JSON.stringify(payload, null, 2));
|
|
209
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"rule_id": "a11y-img-alt",
|
|
4
|
+
"pillar": "a11y",
|
|
5
|
+
"default_severity": "warning",
|
|
6
|
+
"description": "`<img>` must include an `alt` attribute for screen readers.",
|
|
7
|
+
"fix_hint": "Add alt=\"\" for decorative images or a descriptive alt for meaningful images."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"rule_id": "a11y-button-name",
|
|
11
|
+
"pillar": "a11y",
|
|
12
|
+
"default_severity": "warning",
|
|
13
|
+
"description": "`<button>` must have an accessible name (text content or aria-label).",
|
|
14
|
+
"fix_hint": "Add visible text, aria-label, or aria-labelledby."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"rule_id": "a11y-input-label",
|
|
18
|
+
"pillar": "a11y",
|
|
19
|
+
"default_severity": "warning",
|
|
20
|
+
"description": "`<input>` should have an associated label or aria-label.",
|
|
21
|
+
"fix_hint": "Wrap with <Label htmlFor=...> or add aria-label."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"rule_id": "a11y-anchor-href",
|
|
25
|
+
"pillar": "a11y",
|
|
26
|
+
"default_severity": "warning",
|
|
27
|
+
"description": "`<a>` should have a meaningful href.",
|
|
28
|
+
"fix_hint": "Use a valid href or replace with <button> for actions."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"rule_id": "a11y-anchor-placeholder-href",
|
|
32
|
+
"pillar": "a11y",
|
|
33
|
+
"default_severity": "warning",
|
|
34
|
+
"description": "`<a href=\"#\">` is a placeholder link pattern.",
|
|
35
|
+
"fix_hint": "Use <button type=\"button\"> or a real destination URL."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"rule_id": "a11y-dark-mode-contrast",
|
|
39
|
+
"pillar": "a11y",
|
|
40
|
+
"default_severity": "warning",
|
|
41
|
+
"description": "Light-mode color classes may lack a dark: variant.",
|
|
42
|
+
"fix_hint": "Add matching dark: text/background utilities or theme tokens."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"rule_id": "token-hardcoded-color",
|
|
46
|
+
"pillar": "token",
|
|
47
|
+
"default_severity": "warning",
|
|
48
|
+
"description": "Hardcoded hex/rgb color instead of design tokens.",
|
|
49
|
+
"fix_hint": "Replace with a CSS variable or Tailwind theme utility from css_tokens."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"rule_id": "token-tailwind-arbitrary",
|
|
53
|
+
"pillar": "token",
|
|
54
|
+
"default_severity": "warning",
|
|
55
|
+
"description": "Tailwind arbitrary value (e.g. bg-[#fff]) bypasses the token system.",
|
|
56
|
+
"fix_hint": "Use a named theme utility or CSS custom property."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"rule_id": "token-unused-css-var",
|
|
60
|
+
"pillar": "token",
|
|
61
|
+
"default_severity": "info",
|
|
62
|
+
"description": "CSS custom property defined but never referenced.",
|
|
63
|
+
"fix_hint": "Remove unused token or reference it via var(--name) or Tailwind."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"rule_id": "deprecated-component",
|
|
67
|
+
"pillar": "usage",
|
|
68
|
+
"default_severity": "warning",
|
|
69
|
+
"description": "Component is listed as deprecated in .dslinter.json.",
|
|
70
|
+
"fix_hint": "Replace with the canonical component from the catalog (check usage_by_component)."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"rule_id": "duplicate-component",
|
|
74
|
+
"pillar": "usage",
|
|
75
|
+
"default_severity": "warning",
|
|
76
|
+
"description": "Same component name is defined in multiple files.",
|
|
77
|
+
"fix_hint": "Consolidate into one definition and update imports."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"rule_id": "unused-prop",
|
|
81
|
+
"pillar": "usage",
|
|
82
|
+
"default_severity": "info",
|
|
83
|
+
"description": "Declared prop is never passed at any call site.",
|
|
84
|
+
"fix_hint": "Remove unused prop from the API or document why it exists."
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"rule_id": "variant-explosion",
|
|
88
|
+
"pillar": "usage",
|
|
89
|
+
"default_severity": "info",
|
|
90
|
+
"description": "Component has an unusually large prop surface at call sites.",
|
|
91
|
+
"fix_hint": "Split into smaller components or reduce optional props."
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"rule_id": "code-console",
|
|
95
|
+
"pillar": "code",
|
|
96
|
+
"default_severity": "info",
|
|
97
|
+
"description": "console.log (or similar) left in component code.",
|
|
98
|
+
"fix_hint": "Remove debug logging before shipping."
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"rule_id": "code-console-error",
|
|
102
|
+
"pillar": "code",
|
|
103
|
+
"default_severity": "warning",
|
|
104
|
+
"description": "console.error in component code.",
|
|
105
|
+
"fix_hint": "Use proper error handling or remove."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"rule_id": "code-debugger",
|
|
109
|
+
"pillar": "code",
|
|
110
|
+
"default_severity": "error",
|
|
111
|
+
"description": "debugger statement in source.",
|
|
112
|
+
"fix_hint": "Remove debugger."
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"rule_id": "code-inline-style",
|
|
116
|
+
"pillar": "code",
|
|
117
|
+
"default_severity": "warning",
|
|
118
|
+
"description": "Inline JSX style={{}} bypasses token/Tailwind conventions.",
|
|
119
|
+
"fix_hint": "Use className with theme utilities."
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"rule_id": "code-empty-catch",
|
|
123
|
+
"pillar": "code",
|
|
124
|
+
"default_severity": "warning",
|
|
125
|
+
"description": "Empty catch block swallows errors.",
|
|
126
|
+
"fix_hint": "Handle or rethrow the error."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"rule_id": "code-large-file",
|
|
130
|
+
"pillar": "code",
|
|
131
|
+
"default_severity": "info",
|
|
132
|
+
"description": "File exceeds size heuristic for maintainability.",
|
|
133
|
+
"fix_hint": "Split into smaller modules."
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"rule_id": "code-redundant-fragment",
|
|
137
|
+
"pillar": "code",
|
|
138
|
+
"default_severity": "info",
|
|
139
|
+
"description": "Redundant React fragment wrapper.",
|
|
140
|
+
"fix_hint": "Remove unnecessary <>...</>."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"rule_id": "code-suppression-comment",
|
|
144
|
+
"pillar": "code",
|
|
145
|
+
"default_severity": "info",
|
|
146
|
+
"description": "Lint/TS suppression comment present.",
|
|
147
|
+
"fix_hint": "Fix the underlying issue instead of suppressing."
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"rule_id": "code-todo-marker",
|
|
151
|
+
"pillar": "code",
|
|
152
|
+
"default_severity": "info",
|
|
153
|
+
"description": "TODO/FIXME marker in source.",
|
|
154
|
+
"fix_hint": "Resolve or track in issue tracker."
|
|
155
|
+
}
|
|
156
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import catalog from "./rule-catalog.json";
|
|
2
|
+
import type { Severity } from "../types/report";
|
|
3
|
+
|
|
4
|
+
export type RulePillar = "a11y" | "token" | "usage" | "code";
|
|
5
|
+
|
|
6
|
+
export type RuleCatalogEntry = {
|
|
7
|
+
rule_id: string;
|
|
8
|
+
pillar: RulePillar;
|
|
9
|
+
default_severity: Severity;
|
|
10
|
+
description: string;
|
|
11
|
+
fix_hint: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const entries = catalog as RuleCatalogEntry[];
|
|
15
|
+
|
|
16
|
+
const byId = new Map(entries.map((e) => [e.rule_id, e]));
|
|
17
|
+
|
|
18
|
+
export function ruleCatalog(): RuleCatalogEntry[] {
|
|
19
|
+
return entries;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ruleById(ruleId: string): RuleCatalogEntry | undefined {
|
|
23
|
+
return byId.get(ruleId);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function pillarForRule(ruleId: string): RulePillar {
|
|
27
|
+
const entry = byId.get(ruleId);
|
|
28
|
+
if (entry) return entry.pillar;
|
|
29
|
+
if (ruleId.startsWith("a11y-")) return "a11y";
|
|
30
|
+
if (ruleId.startsWith("token-")) return "token";
|
|
31
|
+
if (ruleId.startsWith("code-")) return "code";
|
|
32
|
+
return "usage";
|
|
33
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const scanInput = z.object({
|
|
4
|
+
fresh: z.boolean().optional(),
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const catalogInput = z.object({
|
|
8
|
+
query: z.string().optional(),
|
|
9
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const componentInput = z.object({
|
|
13
|
+
name: z.string().min(1),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const findingsInput = z.object({
|
|
17
|
+
component: z.string().optional(),
|
|
18
|
+
rule_prefix: z.string().optional(),
|
|
19
|
+
severity: z.enum(["error", "warning", "info"]).optional(),
|
|
20
|
+
path: z.string().optional(),
|
|
21
|
+
limit: z.number().int().positive().max(200).optional(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const usageExamplesInput = z.object({
|
|
25
|
+
component: z.string().min(1),
|
|
26
|
+
limit: z.number().int().positive().max(50).optional(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const tokensInput = z.object({
|
|
30
|
+
category: z
|
|
31
|
+
.enum(["color", "spacing", "radius", "typography", "other"])
|
|
32
|
+
.optional(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export const agentContextInput = z.object({
|
|
36
|
+
max_components: z.number().int().positive().max(100).optional(),
|
|
37
|
+
format: z.enum(["markdown", "json"]).optional(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export const checkPathsInput = z.object({
|
|
41
|
+
paths: z.array(z.string().min(1)).min(1).max(50),
|
|
42
|
+
fresh: z.boolean().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const diffSinceInput = z.object({
|
|
46
|
+
save_baseline: z.boolean().optional(),
|
|
47
|
+
fresh: z.boolean().optional(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const suggestFixInput = z.object({
|
|
51
|
+
rule_id: z.string().min(1),
|
|
52
|
+
path: z.string().optional(),
|
|
53
|
+
component: z.string().optional(),
|
|
54
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { buildMcpConfig } from "./config";
|
|
6
|
+
import { ReportCache } from "./report-cache";
|
|
7
|
+
import { createDslinterMcpServer } from "./server";
|
|
8
|
+
import { ruleCatalog } from "./rule-catalog";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const demoRoot = join(__dirname, "../../../../demo");
|
|
12
|
+
const demoReportPath = join(demoRoot, "public/dslinter-report.json");
|
|
13
|
+
|
|
14
|
+
describe("MCP server", () => {
|
|
15
|
+
it("creates server with tools registered", () => {
|
|
16
|
+
const config = buildMcpConfig({
|
|
17
|
+
scanPath: demoRoot,
|
|
18
|
+
projectRoot: demoRoot,
|
|
19
|
+
reportPath: demoReportPath,
|
|
20
|
+
});
|
|
21
|
+
const cache = new ReportCache(config);
|
|
22
|
+
const server = createDslinterMcpServer(config, cache);
|
|
23
|
+
expect(server).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("rule catalog has expected entries", () => {
|
|
27
|
+
const rules = ruleCatalog();
|
|
28
|
+
expect(rules.length).toBeGreaterThanOrEqual(20);
|
|
29
|
+
expect(rules.some((r) => r.rule_id === "deprecated-component")).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("loads demo report through cache", async () => {
|
|
33
|
+
const config = buildMcpConfig({
|
|
34
|
+
scanPath: demoRoot,
|
|
35
|
+
projectRoot: demoRoot,
|
|
36
|
+
reportPath: demoReportPath,
|
|
37
|
+
ttlMs: 999_999_999,
|
|
38
|
+
});
|
|
39
|
+
const cache = new ReportCache(config);
|
|
40
|
+
const report = await cache.getReport();
|
|
41
|
+
expect(report.files.length).toBeGreaterThan(0);
|
|
42
|
+
expect(readFileSync(demoReportPath, "utf8").length).toBeGreaterThan(100);
|
|
43
|
+
});
|
|
44
|
+
});
|