dslinter 0.1.0 → 0.1.1
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 +4 -0
- package/bin/lib/dev-banner.mjs +6 -3
- package/bin/lib/dev-banner.test.mjs +19 -3
- package/bin/modes/dev.mjs +19 -7
- package/dashboard-dist/assets/index-B6zsYv3h.js +206 -0
- package/dashboard-dist/assets/index-BhDQfrwA.css +1 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +52 -52
- package/package.json +6 -6
- package/src/components/ComponentInspectPane.tsx +178 -0
- package/src/components/DashboardCommandPalette.tsx +1 -15
- package/src/components/GovernancePane.tsx +3 -3
- package/src/components/Sidebar.tsx +4 -8
- package/src/dashboard/ComponentCatalog.tsx +41 -68
- package/src/dashboard/ComponentPropUsageDetail.tsx +90 -0
- package/src/dashboard/DashboardBody.tsx +3 -3
- package/src/index.ts +7 -0
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +53 -0
- package/src/playground/buildPlaygroundEntriesFromReport.ts +349 -0
- package/src/report/findingsForComponent.ts +24 -0
- package/src/shell/DashboardLayout.tsx +40 -37
- package/src/shell/hashRoute.test.ts +41 -0
- package/src/shell/hashRoute.ts +2 -1
- package/dashboard-dist/assets/index-Pc1to7nD.css +0 -1
- package/dashboard-dist/assets/index-YvDeIoPr.js +0 -205
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Button } from "@/components/ui/button";
|
|
3
|
+
import {
|
|
4
|
+
Table,
|
|
5
|
+
TableBody,
|
|
6
|
+
TableCell,
|
|
7
|
+
TableHead,
|
|
8
|
+
TableHeader,
|
|
9
|
+
TableRow,
|
|
10
|
+
} from "@/components/ui/table";
|
|
11
|
+
import {
|
|
12
|
+
aggregateDeclaredProps,
|
|
13
|
+
aggregateDefinitions,
|
|
14
|
+
} from "../dashboard/aggregate";
|
|
15
|
+
import {
|
|
16
|
+
buildUnusedPropSetForComponent,
|
|
17
|
+
ComponentPropUsageDetail,
|
|
18
|
+
} from "../dashboard/ComponentPropUsageDetail";
|
|
19
|
+
import { ComponentUsageDetails } from "../dashboard/ComponentUsageDetails";
|
|
20
|
+
import { FindingsList } from "../dashboard/FindingsList";
|
|
21
|
+
import { shortPath } from "../dashboard/paths";
|
|
22
|
+
import { findingsForComponent } from "../report/findingsForComponent";
|
|
23
|
+
import type { WorkspaceReport } from "../types/report";
|
|
24
|
+
import { Section } from "./Section";
|
|
25
|
+
|
|
26
|
+
type Props = {
|
|
27
|
+
componentId: string;
|
|
28
|
+
workspaceReport: WorkspaceReport | null;
|
|
29
|
+
reportReady: boolean;
|
|
30
|
+
hasPlaygroundSpec: boolean;
|
|
31
|
+
onBackToGovernance: () => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function ComponentInspectPane({
|
|
35
|
+
componentId,
|
|
36
|
+
workspaceReport,
|
|
37
|
+
reportReady,
|
|
38
|
+
hasPlaygroundSpec,
|
|
39
|
+
onBackToGovernance,
|
|
40
|
+
}: Props) {
|
|
41
|
+
const definitions = useMemo(() => {
|
|
42
|
+
if (!workspaceReport) return [];
|
|
43
|
+
return aggregateDefinitions(workspaceReport).get(componentId) ?? [];
|
|
44
|
+
}, [workspaceReport, componentId]);
|
|
45
|
+
|
|
46
|
+
const declared = useMemo(() => {
|
|
47
|
+
if (!workspaceReport) return [];
|
|
48
|
+
return aggregateDeclaredProps(workspaceReport).get(componentId) ?? [];
|
|
49
|
+
}, [workspaceReport, componentId]);
|
|
50
|
+
|
|
51
|
+
const unusedProps = useMemo(() => {
|
|
52
|
+
if (!workspaceReport) return new Set<string>();
|
|
53
|
+
return buildUnusedPropSetForComponent(
|
|
54
|
+
workspaceReport,
|
|
55
|
+
componentId,
|
|
56
|
+
declared,
|
|
57
|
+
);
|
|
58
|
+
}, [workspaceReport, componentId, declared]);
|
|
59
|
+
|
|
60
|
+
const findings = useMemo(
|
|
61
|
+
() => findingsForComponent(workspaceReport, componentId),
|
|
62
|
+
[workspaceReport, componentId],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const previewNote = hasPlaygroundSpec
|
|
66
|
+
? "A preview was expected for this component but the module could not be loaded in the dashboard bundle (check the file path, export name, or that npx dslinter was run from the project root)."
|
|
67
|
+
: "No playable component definition was found in a .tsx/.jsx file under the scanned workspace — only names from JSX usage appear in the catalog.";
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden bg-background">
|
|
71
|
+
<div className="min-h-0 flex-1 overflow-auto">
|
|
72
|
+
<header className="border-b border-border bg-card px-8 py-6">
|
|
73
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
74
|
+
<div className="min-w-0">
|
|
75
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
76
|
+
Components
|
|
77
|
+
</p>
|
|
78
|
+
<h1 className="text-3xl font-semibold tracking-tight text-foreground">
|
|
79
|
+
{componentId}
|
|
80
|
+
</h1>
|
|
81
|
+
<p className="mt-2 max-w-2xl text-sm text-muted-foreground">
|
|
82
|
+
Scan snapshot — no live preview. {previewNote}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
<Button type="button" size="sm" variant="outline" onClick={onBackToGovernance}>
|
|
86
|
+
Back to governance
|
|
87
|
+
</Button>
|
|
88
|
+
</div>
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
<div className="mx-auto w-full max-w-5xl space-y-10 px-8 py-8">
|
|
92
|
+
<Section
|
|
93
|
+
id="definitions"
|
|
94
|
+
title="Definitions"
|
|
95
|
+
description="Source files where this component is defined."
|
|
96
|
+
>
|
|
97
|
+
{definitions.length > 0 ? (
|
|
98
|
+
<Table>
|
|
99
|
+
<TableHeader>
|
|
100
|
+
<TableRow>
|
|
101
|
+
<TableHead>File</TableHead>
|
|
102
|
+
<TableHead className="w-20">Line</TableHead>
|
|
103
|
+
<TableHead>Kind</TableHead>
|
|
104
|
+
</TableRow>
|
|
105
|
+
</TableHeader>
|
|
106
|
+
<TableBody>
|
|
107
|
+
{definitions.map((site) => (
|
|
108
|
+
<TableRow
|
|
109
|
+
key={`${site.path}:${site.line}:${site.kind}`}
|
|
110
|
+
>
|
|
111
|
+
<TableCell className="font-mono text-xs">
|
|
112
|
+
{workspaceReport
|
|
113
|
+
? shortPath(workspaceReport.root, site.path)
|
|
114
|
+
: site.path}
|
|
115
|
+
</TableCell>
|
|
116
|
+
<TableCell>{site.line}</TableCell>
|
|
117
|
+
<TableCell className="text-muted-foreground">
|
|
118
|
+
{site.kind}
|
|
119
|
+
</TableCell>
|
|
120
|
+
</TableRow>
|
|
121
|
+
))}
|
|
122
|
+
</TableBody>
|
|
123
|
+
</Table>
|
|
124
|
+
) : (
|
|
125
|
+
<p className="text-sm text-muted-foreground">
|
|
126
|
+
No definition sites recorded — this name may appear only from JSX
|
|
127
|
+
usage.
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
</Section>
|
|
131
|
+
|
|
132
|
+
<Section
|
|
133
|
+
id="props"
|
|
134
|
+
title="Props"
|
|
135
|
+
description="Declared props and workspace usage from the latest scan."
|
|
136
|
+
>
|
|
137
|
+
{reportReady && workspaceReport ? (
|
|
138
|
+
<ComponentPropUsageDetail
|
|
139
|
+
component={componentId}
|
|
140
|
+
declared={declared}
|
|
141
|
+
unusedProps={unusedProps}
|
|
142
|
+
/>
|
|
143
|
+
) : (
|
|
144
|
+
<p className="text-sm text-muted-foreground">
|
|
145
|
+
Load the DSLinter report to see prop usage.
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
</Section>
|
|
149
|
+
|
|
150
|
+
<Section
|
|
151
|
+
id="usage"
|
|
152
|
+
title="App usage"
|
|
153
|
+
description="Where this component is used in the workspace."
|
|
154
|
+
>
|
|
155
|
+
<ComponentUsageDetails
|
|
156
|
+
report={workspaceReport}
|
|
157
|
+
componentId={componentId}
|
|
158
|
+
/>
|
|
159
|
+
</Section>
|
|
160
|
+
|
|
161
|
+
<Section
|
|
162
|
+
id="findings"
|
|
163
|
+
title="Findings"
|
|
164
|
+
description="DSLinter findings on files where this component is defined."
|
|
165
|
+
>
|
|
166
|
+
{reportReady && workspaceReport ? (
|
|
167
|
+
<FindingsList findings={findings} root={workspaceReport.root} />
|
|
168
|
+
) : (
|
|
169
|
+
<p className="text-sm text-muted-foreground">
|
|
170
|
+
Load the DSLinter report to see findings.
|
|
171
|
+
</p>
|
|
172
|
+
)}
|
|
173
|
+
</Section>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -13,7 +13,6 @@ import type { HashRoute } from "../shell/hashRoute";
|
|
|
13
13
|
|
|
14
14
|
type Props = {
|
|
15
15
|
catalogNames: string[];
|
|
16
|
-
playgroundIds: Set<string>;
|
|
17
16
|
onNavigate: (next: HashRoute) => void;
|
|
18
17
|
open: boolean;
|
|
19
18
|
onOpenChange: (open: boolean) => void;
|
|
@@ -26,21 +25,8 @@ function eventTargetIsEditable(target: EventTarget | null): boolean {
|
|
|
26
25
|
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
function navigateToCatalogName(
|
|
30
|
-
name: string,
|
|
31
|
-
playgroundIds: Set<string>,
|
|
32
|
-
onNavigate: (next: HashRoute) => void,
|
|
33
|
-
) {
|
|
34
|
-
if (playgroundIds.has(name)) {
|
|
35
|
-
onNavigate({ view: "component", componentId: name });
|
|
36
|
-
} else {
|
|
37
|
-
onNavigate({ view: "governance", catalog: name });
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
28
|
export function DashboardCommandPalette({
|
|
42
29
|
catalogNames,
|
|
43
|
-
playgroundIds,
|
|
44
30
|
onNavigate,
|
|
45
31
|
open,
|
|
46
32
|
onOpenChange,
|
|
@@ -86,7 +72,7 @@ export function DashboardCommandPalette({
|
|
|
86
72
|
key={name}
|
|
87
73
|
value={name}
|
|
88
74
|
onSelect={() => {
|
|
89
|
-
|
|
75
|
+
onNavigate({ view: "component", componentId: name });
|
|
90
76
|
close();
|
|
91
77
|
}}
|
|
92
78
|
>
|
|
@@ -9,7 +9,7 @@ type Props = {
|
|
|
9
9
|
reportUrl?: string;
|
|
10
10
|
dslinterReportHint?: string;
|
|
11
11
|
dslinterReport: DslinterReportState;
|
|
12
|
-
|
|
12
|
+
onOpenComponent?: (name: string) => void;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export function GovernancePane({
|
|
@@ -17,7 +17,7 @@ export function GovernancePane({
|
|
|
17
17
|
reportUrl: _reportUrl = "/dslint-report.json",
|
|
18
18
|
dslinterReportHint = "npm run dslint:report",
|
|
19
19
|
dslinterReport,
|
|
20
|
-
|
|
20
|
+
onOpenComponent,
|
|
21
21
|
}: Props) {
|
|
22
22
|
const { report, error, loading } = dslinterReport;
|
|
23
23
|
const componentCatalogCount = report
|
|
@@ -80,7 +80,7 @@ export function GovernancePane({
|
|
|
80
80
|
</p>
|
|
81
81
|
</header>
|
|
82
82
|
<div className="min-w-0 w-full px-6 py-8">
|
|
83
|
-
<DashboardBody report={report}
|
|
83
|
+
<DashboardBody report={report} onOpenComponent={onOpenComponent} />
|
|
84
84
|
</div>
|
|
85
85
|
</div>
|
|
86
86
|
);
|
|
@@ -9,7 +9,6 @@ import type { DashboardThemePreference } from "../shell/DashboardLayout";
|
|
|
9
9
|
|
|
10
10
|
type Props = {
|
|
11
11
|
report: WorkspaceReport | null;
|
|
12
|
-
playgroundIds: Set<string>;
|
|
13
12
|
reportLoading: boolean;
|
|
14
13
|
reportError: string | null;
|
|
15
14
|
route: HashRoute;
|
|
@@ -50,7 +49,6 @@ function sectionLabel(text: string) {
|
|
|
50
49
|
|
|
51
50
|
export function Sidebar({
|
|
52
51
|
report,
|
|
53
|
-
playgroundIds,
|
|
54
52
|
reportLoading,
|
|
55
53
|
reportError,
|
|
56
54
|
route,
|
|
@@ -64,7 +62,8 @@ export function Sidebar({
|
|
|
64
62
|
[report],
|
|
65
63
|
);
|
|
66
64
|
const tokensActive = route.view === "tokens";
|
|
67
|
-
const governanceActive =
|
|
65
|
+
const governanceActive =
|
|
66
|
+
route.view === "governance" && route.catalog == null;
|
|
68
67
|
|
|
69
68
|
const onThemeValueChange = (value: string) => {
|
|
70
69
|
if (value !== "light" && value !== "dark") return;
|
|
@@ -173,16 +172,13 @@ export function Sidebar({
|
|
|
173
172
|
) : null}
|
|
174
173
|
{catalogNames.map((name) => {
|
|
175
174
|
const active =
|
|
176
|
-
|
|
177
|
-
(route.view === "governance" && route.catalog === name);
|
|
175
|
+
route.view === "component" && route.componentId === name;
|
|
178
176
|
return (
|
|
179
177
|
<button
|
|
180
178
|
key={name}
|
|
181
179
|
type="button"
|
|
182
180
|
onClick={() =>
|
|
183
|
-
|
|
184
|
-
? onNavigate({ view: "component", componentId: name })
|
|
185
|
-
: onNavigate({ view: "governance", catalog: name })
|
|
181
|
+
onNavigate({ view: "component", componentId: name })
|
|
186
182
|
}
|
|
187
183
|
className={navButtonClass(active)}
|
|
188
184
|
>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
HoverCard,
|
|
4
4
|
HoverCardContent,
|
|
@@ -16,9 +16,13 @@ import {
|
|
|
16
16
|
aggregateDeclaredProps,
|
|
17
17
|
aggregateDefinitions,
|
|
18
18
|
catalogComponentNames,
|
|
19
|
-
catalogRowDomId,
|
|
20
19
|
usageMap,
|
|
21
20
|
} from "./aggregate";
|
|
21
|
+
import {
|
|
22
|
+
catalogAttributeProps,
|
|
23
|
+
ComponentPropUsageDetail,
|
|
24
|
+
buildUnusedPropSetForComponent,
|
|
25
|
+
} from "./ComponentPropUsageDetail";
|
|
22
26
|
import { shortPath } from "./paths";
|
|
23
27
|
import type { WorkspaceReport } from "../types/report";
|
|
24
28
|
import { pluralize } from "usemods";
|
|
@@ -26,26 +30,15 @@ import { pluralize } from "usemods";
|
|
|
26
30
|
/** Set of `"ComponentName/propName"` keys for every declared prop with no recorded usage. */
|
|
27
31
|
function buildUnusedPropSet(report: WorkspaceReport): Set<string> {
|
|
28
32
|
const s = new Set<string>();
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const definition of file.definitions ?? []) {
|
|
38
|
-
const componentName = definition.name;
|
|
39
|
-
const propFrequencies = usageByComponent.get(componentName) ?? {};
|
|
40
|
-
|
|
41
|
-
for (const propName of definition.declared_props ?? []) {
|
|
42
|
-
if ((propFrequencies[propName] ?? 0) === 0) {
|
|
43
|
-
s.add(`${componentName}/${propName}`);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
33
|
+
const declaredByName = aggregateDeclaredProps(report);
|
|
34
|
+
for (const [componentName, declared] of declaredByName) {
|
|
35
|
+
const unused = buildUnusedPropSetForComponent(
|
|
36
|
+
report,
|
|
37
|
+
componentName,
|
|
38
|
+
declared,
|
|
39
|
+
);
|
|
40
|
+
for (const key of unused) s.add(key);
|
|
47
41
|
}
|
|
48
|
-
|
|
49
42
|
return s;
|
|
50
43
|
}
|
|
51
44
|
|
|
@@ -63,51 +56,24 @@ function CatalogPropUsageHover({
|
|
|
63
56
|
unusedProps: Set<string>;
|
|
64
57
|
usedPropCount: number;
|
|
65
58
|
}) {
|
|
66
|
-
const
|
|
67
|
-
(prop) => !unusedProps.has(`${component}/${prop}`),
|
|
68
|
-
);
|
|
69
|
-
const unused = declared.filter((prop) =>
|
|
70
|
-
unusedProps.has(`${component}/${prop}`),
|
|
71
|
-
);
|
|
59
|
+
const attributeProps = catalogAttributeProps(declared);
|
|
72
60
|
|
|
73
61
|
return (
|
|
74
62
|
<HoverCard openDelay={200} closeDelay={100}>
|
|
75
63
|
<HoverCardTrigger asChild>
|
|
76
64
|
<button type="button" className={catalogHoverTriggerClass}>
|
|
77
65
|
<span className="text-muted-foreground">
|
|
78
|
-
{usedPropCount}/{
|
|
79
|
-
used
|
|
66
|
+
{usedPropCount}/{attributeProps.length}{" "}
|
|
67
|
+
{pluralize("prop", usedPropCount)} used
|
|
80
68
|
</span>
|
|
81
69
|
</button>
|
|
82
70
|
</HoverCardTrigger>
|
|
83
71
|
<HoverCardContent align="start" className="w-56 p-3">
|
|
84
|
-
<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
</p>
|
|
90
|
-
<ul className="mt-1 space-y-0.5 font-mono text-xs text-foreground">
|
|
91
|
-
{used.map((prop) => (
|
|
92
|
-
<li key={prop}>{prop}</li>
|
|
93
|
-
))}
|
|
94
|
-
</ul>
|
|
95
|
-
</div>
|
|
96
|
-
) : null}
|
|
97
|
-
{unused.length > 0 ? (
|
|
98
|
-
<div className={used.length > 0 ? "mt-3" : "mt-2"}>
|
|
99
|
-
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
100
|
-
Never passed
|
|
101
|
-
</p>
|
|
102
|
-
<ul className="mt-1 space-y-0.5 font-mono text-xs text-muted-foreground/70">
|
|
103
|
-
{unused.map((prop) => (
|
|
104
|
-
<li key={prop} className="line-through">
|
|
105
|
-
{prop}
|
|
106
|
-
</li>
|
|
107
|
-
))}
|
|
108
|
-
</ul>
|
|
109
|
-
</div>
|
|
110
|
-
) : null}
|
|
72
|
+
<ComponentPropUsageDetail
|
|
73
|
+
component={component}
|
|
74
|
+
declared={declared}
|
|
75
|
+
unusedProps={unusedProps}
|
|
76
|
+
/>
|
|
111
77
|
</HoverCardContent>
|
|
112
78
|
</HoverCard>
|
|
113
79
|
);
|
|
@@ -149,10 +115,10 @@ function CatalogAppUsageHover({
|
|
|
149
115
|
|
|
150
116
|
export function ComponentCatalog({
|
|
151
117
|
report,
|
|
152
|
-
|
|
118
|
+
onOpenComponent,
|
|
153
119
|
}: {
|
|
154
120
|
report: WorkspaceReport;
|
|
155
|
-
|
|
121
|
+
onOpenComponent?: (name: string) => void;
|
|
156
122
|
}) {
|
|
157
123
|
const defs = aggregateDefinitions(report);
|
|
158
124
|
const usages = usageMap(report);
|
|
@@ -163,12 +129,6 @@ export function ComponentCatalog({
|
|
|
163
129
|
[report],
|
|
164
130
|
);
|
|
165
131
|
|
|
166
|
-
useEffect(() => {
|
|
167
|
-
if (!focusName || !names.includes(focusName)) return;
|
|
168
|
-
const el = document.getElementById(catalogRowDomId(focusName));
|
|
169
|
-
el?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
170
|
-
}, [focusName, names]);
|
|
171
|
-
|
|
172
132
|
return (
|
|
173
133
|
<Table>
|
|
174
134
|
<TableHeader>
|
|
@@ -182,16 +142,29 @@ export function ComponentCatalog({
|
|
|
182
142
|
{names.map((name) => {
|
|
183
143
|
const use = usages.get(name);
|
|
184
144
|
const declared = declaredByName.get(name) ?? [];
|
|
185
|
-
const
|
|
145
|
+
const attributeProps = catalogAttributeProps(declared);
|
|
146
|
+
const usedPropCount = attributeProps.filter(
|
|
186
147
|
(prop) => !unusedProps.has(`${name}/${prop}`),
|
|
187
148
|
).length;
|
|
188
149
|
|
|
189
150
|
return (
|
|
190
|
-
<TableRow key={name}
|
|
191
|
-
<TableCell>
|
|
151
|
+
<TableRow key={name}>
|
|
152
|
+
<TableCell>
|
|
153
|
+
{onOpenComponent ? (
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
onClick={() => onOpenComponent(name)}
|
|
157
|
+
className="text-left font-medium text-foreground underline decoration-transparent underline-offset-2 transition hover:decoration-current"
|
|
158
|
+
>
|
|
159
|
+
{name}
|
|
160
|
+
</button>
|
|
161
|
+
) : (
|
|
162
|
+
name
|
|
163
|
+
)}
|
|
164
|
+
</TableCell>
|
|
192
165
|
|
|
193
166
|
<TableCell>
|
|
194
|
-
{
|
|
167
|
+
{attributeProps.length > 0 ? (
|
|
195
168
|
<CatalogPropUsageHover
|
|
196
169
|
component={name}
|
|
197
170
|
declared={declared}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { WorkspaceReport } from "../types/report";
|
|
2
|
+
import { pluralize } from "usemods";
|
|
3
|
+
|
|
4
|
+
/** React slot props — not attribute props. */
|
|
5
|
+
const SLOT_PROPS = new Set(["children"]);
|
|
6
|
+
|
|
7
|
+
export function catalogAttributeProps(props: string[]): string[] {
|
|
8
|
+
return props.filter((prop) => !SLOT_PROPS.has(prop));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Set of `"ComponentName/propName"` keys for props with no recorded usage. */
|
|
12
|
+
export function buildUnusedPropSetForComponent(
|
|
13
|
+
report: WorkspaceReport,
|
|
14
|
+
componentName: string,
|
|
15
|
+
declared: string[],
|
|
16
|
+
): Set<string> {
|
|
17
|
+
const s = new Set<string>();
|
|
18
|
+
const usageRow = (report.usage_by_component ?? []).find(
|
|
19
|
+
(u) => u.component === componentName,
|
|
20
|
+
);
|
|
21
|
+
const propFrequencies = usageRow?.prop_frequencies ?? {};
|
|
22
|
+
for (const propName of catalogAttributeProps(declared)) {
|
|
23
|
+
if ((propFrequencies[propName] ?? 0) === 0) {
|
|
24
|
+
s.add(`${componentName}/${propName}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return s;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ComponentPropUsageDetail({
|
|
31
|
+
component,
|
|
32
|
+
declared,
|
|
33
|
+
unusedProps,
|
|
34
|
+
}: {
|
|
35
|
+
component: string;
|
|
36
|
+
declared: string[];
|
|
37
|
+
unusedProps: Set<string>;
|
|
38
|
+
}) {
|
|
39
|
+
const attributeProps = catalogAttributeProps(declared);
|
|
40
|
+
const used = attributeProps.filter(
|
|
41
|
+
(prop) => !unusedProps.has(`${component}/${prop}`),
|
|
42
|
+
);
|
|
43
|
+
const unused = attributeProps.filter((prop) =>
|
|
44
|
+
unusedProps.has(`${component}/${prop}`),
|
|
45
|
+
);
|
|
46
|
+
const usedPropCount = used.length;
|
|
47
|
+
|
|
48
|
+
if (attributeProps.length === 0) {
|
|
49
|
+
return (
|
|
50
|
+
<p className="text-sm text-muted-foreground">
|
|
51
|
+
No declared props recorded for this component.
|
|
52
|
+
</p>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="space-y-4">
|
|
58
|
+
<p className="text-sm text-muted-foreground">
|
|
59
|
+
{usedPropCount}/{attributeProps.length}{" "}
|
|
60
|
+
{pluralize("prop", usedPropCount)} used in the workspace snapshot.
|
|
61
|
+
</p>
|
|
62
|
+
{used.length > 0 ? (
|
|
63
|
+
<div>
|
|
64
|
+
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
65
|
+
Used
|
|
66
|
+
</p>
|
|
67
|
+
<ul className="mt-1 space-y-0.5 font-mono text-xs text-foreground">
|
|
68
|
+
{used.map((prop) => (
|
|
69
|
+
<li key={prop}>{prop}</li>
|
|
70
|
+
))}
|
|
71
|
+
</ul>
|
|
72
|
+
</div>
|
|
73
|
+
) : null}
|
|
74
|
+
{unused.length > 0 ? (
|
|
75
|
+
<div>
|
|
76
|
+
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
77
|
+
Never passed
|
|
78
|
+
</p>
|
|
79
|
+
<ul className="mt-1 space-y-0.5 font-mono text-xs text-muted-foreground/70">
|
|
80
|
+
{unused.map((prop) => (
|
|
81
|
+
<li key={prop} className="line-through">
|
|
82
|
+
{prop}
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
</div>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -14,10 +14,10 @@ import { ScoreStrip } from "./ScoreStrip";
|
|
|
14
14
|
|
|
15
15
|
export function DashboardBody({
|
|
16
16
|
report,
|
|
17
|
-
|
|
17
|
+
onOpenComponent,
|
|
18
18
|
}: {
|
|
19
19
|
report: WorkspaceReport;
|
|
20
|
-
|
|
20
|
+
onOpenComponent?: (name: string) => void;
|
|
21
21
|
}) {
|
|
22
22
|
return (
|
|
23
23
|
<div className="space-y-10">
|
|
@@ -62,7 +62,7 @@ export function DashboardBody({
|
|
|
62
62
|
title="Components"
|
|
63
63
|
description="Definitions and JSX usage from the latest snapshot."
|
|
64
64
|
>
|
|
65
|
-
<ComponentCatalog report={report}
|
|
65
|
+
<ComponentCatalog report={report} onOpenComponent={onOpenComponent} />
|
|
66
66
|
</Section>
|
|
67
67
|
|
|
68
68
|
<Section
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,12 @@ export type {
|
|
|
13
13
|
} from "./types/controls";
|
|
14
14
|
export { defaultArgsFromControls } from "./types/controls";
|
|
15
15
|
export { definePlayground } from "./playground/definePlayground";
|
|
16
|
+
export {
|
|
17
|
+
buildPlaygroundEntriesFromReport,
|
|
18
|
+
playgroundCatalogId,
|
|
19
|
+
resolvePlaygroundEntry,
|
|
20
|
+
} from "./playground/buildPlaygroundEntriesFromReport";
|
|
21
|
+
export type { BuildPlaygroundModules, BuildPlaygroundOptions } from "./playground/buildPlaygroundEntriesFromReport";
|
|
16
22
|
export type { DefinedPlayground } from "./playground/definePlayground";
|
|
17
23
|
export type { PlaygroundPreviewProps, PlaygroundPreviewComponent } from "./types/preview";
|
|
18
24
|
export type {
|
|
@@ -69,6 +75,7 @@ export { a11ySummaryForModule, resolveModuleSourcePath } from "./report/a11yForM
|
|
|
69
75
|
export type { A11yModuleSummary } from "./report/a11yForModule";
|
|
70
76
|
export { tokenStyleFindingsForModule } from "./report/tokenStyleFindingsForModule";
|
|
71
77
|
export { codeScoreSummaryForModule } from "./report/codeScoreForModule";
|
|
78
|
+
export { findingsForComponent } from "./report/findingsForComponent";
|
|
72
79
|
export type { CodeScoreModuleSummary } from "./report/codeScoreForModule";
|
|
73
80
|
export { TokenWall } from "./dashboard/TokenWall";
|
|
74
81
|
export { DashboardBody } from "./dashboard/DashboardBody";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { PlaygroundEntry } from "../types/playground";
|
|
3
|
+
import type { WorkspaceReport } from "../types/report";
|
|
4
|
+
import { resolvePlaygroundEntry } from "./buildPlaygroundEntriesFromReport";
|
|
5
|
+
|
|
6
|
+
const entries: PlaygroundEntry[] = [
|
|
7
|
+
{
|
|
8
|
+
id: "PrimaryButton",
|
|
9
|
+
meta: { id: "PrimaryButton", title: "PrimaryButton" },
|
|
10
|
+
modulePath: "@dslint-scan/src/components/PrimaryButton.tsx",
|
|
11
|
+
controls: [],
|
|
12
|
+
Preview: () => null,
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
describe("resolvePlaygroundEntry", () => {
|
|
17
|
+
it("finds by catalog export name id", () => {
|
|
18
|
+
expect(resolvePlaygroundEntry(entries, "PrimaryButton")).toBe(entries[0]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns undefined for unknown name", () => {
|
|
22
|
+
expect(resolvePlaygroundEntry(entries, "Missing")).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("playground catalog id alignment", () => {
|
|
27
|
+
it("report playgrounds use export_name as id", () => {
|
|
28
|
+
const report: WorkspaceReport = {
|
|
29
|
+
root: "/repo",
|
|
30
|
+
files: [],
|
|
31
|
+
findings: [],
|
|
32
|
+
scores: {
|
|
33
|
+
system_health: 0,
|
|
34
|
+
token_adoption: 0,
|
|
35
|
+
component_adoption: 0,
|
|
36
|
+
ux_consistency: 0,
|
|
37
|
+
},
|
|
38
|
+
ownership: [],
|
|
39
|
+
duplicate_components: [],
|
|
40
|
+
usage_by_component: [],
|
|
41
|
+
playgrounds: [
|
|
42
|
+
{
|
|
43
|
+
id: "Card",
|
|
44
|
+
export_name: "Card",
|
|
45
|
+
rel_path: "src/components/nested/DuplicateCardA.tsx",
|
|
46
|
+
declared_props: [],
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
expect(report.playgrounds?.[0]?.id).toBe("Card");
|
|
51
|
+
expect(report.playgrounds?.[0]?.export_name).toBe("Card");
|
|
52
|
+
});
|
|
53
|
+
});
|