dslinter 0.0.32 → 0.0.34
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 +24 -0
- package/bin/lib/port-check.mjs +57 -0
- package/bin/lib/project-root.mjs +73 -2
- package/bin/modes/dev.mjs +115 -45
- package/dashboard-dist/assets/index-DIVPHaG0.js +205 -0
- package/dashboard-dist/assets/index-ZswfIdxX.css +1 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +52 -52
- package/package.json +7 -7
- package/src/components/ComponentPlaygroundPane.tsx +2 -2
- package/src/components/DashboardCommandPalette.tsx +21 -43
- package/src/components/GovernancePane.tsx +11 -1
- package/src/components/Sidebar.tsx +50 -23
- package/src/dashboard/ComponentCatalog.tsx +16 -3
- package/src/dashboard/DashboardBody.tsx +8 -2
- package/src/dashboard/aggregate.ts +16 -3
- package/src/dashboard/useWorkspaceReport.ts +14 -7
- package/src/report/modulePathMatch.ts +5 -2
- package/src/shell/DashboardLayout.tsx +28 -2
- package/src/shell/hashRoute.ts +10 -2
- package/dashboard-dist/assets/index-BTfdBwuZ.js +0 -245
- package/dashboard-dist/assets/index-Dgfmp0Yv.css +0 -1
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { IconMoon, IconSearch, IconSun } from "@/components/icons";
|
|
3
3
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { componentCatalogNamesFromReport } from "../dashboard/aggregate";
|
|
6
|
+
import type { WorkspaceReport } from "../types/report";
|
|
6
7
|
import type { HashRoute } from "../shell/hashRoute";
|
|
7
8
|
import type { DashboardThemePreference } from "../shell/DashboardLayout";
|
|
8
9
|
|
|
9
10
|
type Props = {
|
|
10
|
-
|
|
11
|
+
report: WorkspaceReport | null;
|
|
12
|
+
playgroundIds: Set<string>;
|
|
13
|
+
reportLoading: boolean;
|
|
14
|
+
reportError: string | null;
|
|
11
15
|
route: HashRoute;
|
|
12
16
|
onNavigate: (next: HashRoute) => void;
|
|
13
17
|
onOpenCommandPalette: () => void;
|
|
@@ -45,13 +49,20 @@ function sectionLabel(text: string) {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
export function Sidebar({
|
|
48
|
-
|
|
52
|
+
report,
|
|
53
|
+
playgroundIds,
|
|
54
|
+
reportLoading,
|
|
55
|
+
reportError,
|
|
49
56
|
route,
|
|
50
57
|
onNavigate,
|
|
51
58
|
onOpenCommandPalette,
|
|
52
59
|
theme,
|
|
53
60
|
onThemeChange,
|
|
54
61
|
}: Props) {
|
|
62
|
+
const catalogNames = useMemo(
|
|
63
|
+
() => componentCatalogNamesFromReport(report),
|
|
64
|
+
[report],
|
|
65
|
+
);
|
|
55
66
|
const tokensActive = route.view === "tokens";
|
|
56
67
|
const governanceActive = route.view === "governance";
|
|
57
68
|
|
|
@@ -139,28 +150,44 @@ export function Sidebar({
|
|
|
139
150
|
Design tokens
|
|
140
151
|
</button>
|
|
141
152
|
|
|
142
|
-
{sectionLabel(
|
|
153
|
+
{sectionLabel(
|
|
154
|
+
catalogNames.length > 0
|
|
155
|
+
? `Components (${catalogNames.length})`
|
|
156
|
+
: "Components",
|
|
157
|
+
)}
|
|
143
158
|
<div className="space-y-0.5">
|
|
144
|
-
{
|
|
159
|
+
{reportLoading && catalogNames.length === 0 ? (
|
|
160
|
+
<p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">
|
|
161
|
+
Loading components…
|
|
162
|
+
</p>
|
|
163
|
+
) : null}
|
|
164
|
+
{reportError && catalogNames.length === 0 ? (
|
|
165
|
+
<p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">
|
|
166
|
+
Could not load report
|
|
167
|
+
</p>
|
|
168
|
+
) : null}
|
|
169
|
+
{!reportLoading && !reportError && catalogNames.length === 0 ? (
|
|
170
|
+
<p className="px-2.5 py-1.5 text-sm text-sidebar-foreground/50">
|
|
171
|
+
No components in scan
|
|
172
|
+
</p>
|
|
173
|
+
) : null}
|
|
174
|
+
{catalogNames.map((name) => {
|
|
145
175
|
const active =
|
|
146
|
-
route.view === "component" && route.componentId ===
|
|
147
|
-
|
|
148
|
-
const showGroup =
|
|
149
|
-
Boolean(e.meta.group) &&
|
|
150
|
-
(!prev || prev.meta.group !== e.meta.group);
|
|
176
|
+
(route.view === "component" && route.componentId === name) ||
|
|
177
|
+
(route.view === "governance" && route.catalog === name);
|
|
151
178
|
return (
|
|
152
|
-
<
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
onNavigate({ view: "component", componentId:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
</
|
|
179
|
+
<button
|
|
180
|
+
key={name}
|
|
181
|
+
type="button"
|
|
182
|
+
onClick={() =>
|
|
183
|
+
playgroundIds.has(name)
|
|
184
|
+
? onNavigate({ view: "component", componentId: name })
|
|
185
|
+
: onNavigate({ view: "governance", catalog: name })
|
|
186
|
+
}
|
|
187
|
+
className={navButtonClass(active)}
|
|
188
|
+
>
|
|
189
|
+
{name}
|
|
190
|
+
</button>
|
|
164
191
|
);
|
|
165
192
|
})}
|
|
166
193
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
HoverCard,
|
|
4
4
|
HoverCardContent,
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
aggregateDeclaredProps,
|
|
17
17
|
aggregateDefinitions,
|
|
18
18
|
catalogComponentNames,
|
|
19
|
+
catalogRowDomId,
|
|
19
20
|
usageMap,
|
|
20
21
|
} from "./aggregate";
|
|
21
22
|
import { shortPath } from "./paths";
|
|
@@ -146,7 +147,13 @@ function CatalogAppUsageHover({
|
|
|
146
147
|
);
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
export function ComponentCatalog({
|
|
150
|
+
export function ComponentCatalog({
|
|
151
|
+
report,
|
|
152
|
+
focusName,
|
|
153
|
+
}: {
|
|
154
|
+
report: WorkspaceReport;
|
|
155
|
+
focusName?: string;
|
|
156
|
+
}) {
|
|
150
157
|
const defs = aggregateDefinitions(report);
|
|
151
158
|
const usages = usageMap(report);
|
|
152
159
|
const names = catalogComponentNames(defs, usages);
|
|
@@ -156,6 +163,12 @@ export function ComponentCatalog({ report }: { report: WorkspaceReport }) {
|
|
|
156
163
|
[report],
|
|
157
164
|
);
|
|
158
165
|
|
|
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
|
+
|
|
159
172
|
return (
|
|
160
173
|
<Table>
|
|
161
174
|
<TableHeader>
|
|
@@ -174,7 +187,7 @@ export function ComponentCatalog({ report }: { report: WorkspaceReport }) {
|
|
|
174
187
|
).length;
|
|
175
188
|
|
|
176
189
|
return (
|
|
177
|
-
<TableRow key={name}>
|
|
190
|
+
<TableRow key={name} id={catalogRowDomId(name)}>
|
|
178
191
|
<TableCell>{name}</TableCell>
|
|
179
192
|
|
|
180
193
|
<TableCell>
|
|
@@ -12,7 +12,13 @@ import { ComponentCatalog } from "./ComponentCatalog";
|
|
|
12
12
|
import { FindingsList } from "./FindingsList";
|
|
13
13
|
import { ScoreStrip } from "./ScoreStrip";
|
|
14
14
|
|
|
15
|
-
export function DashboardBody({
|
|
15
|
+
export function DashboardBody({
|
|
16
|
+
report,
|
|
17
|
+
focusName,
|
|
18
|
+
}: {
|
|
19
|
+
report: WorkspaceReport;
|
|
20
|
+
focusName?: string;
|
|
21
|
+
}) {
|
|
16
22
|
return (
|
|
17
23
|
<div className="space-y-10">
|
|
18
24
|
<ScoreStrip scores={report.scores} />
|
|
@@ -56,7 +62,7 @@ export function DashboardBody({ report }: { report: WorkspaceReport }) {
|
|
|
56
62
|
title="Components"
|
|
57
63
|
description="Definitions and JSX usage from the latest snapshot."
|
|
58
64
|
>
|
|
59
|
-
<ComponentCatalog report={report} />
|
|
65
|
+
<ComponentCatalog report={report} focusName={focusName} />
|
|
60
66
|
</Section>
|
|
61
67
|
|
|
62
68
|
<Section
|
|
@@ -10,8 +10,8 @@ const HIDDEN_COMPONENTS = new Set(["App", "React.StrictMode"]);
|
|
|
10
10
|
|
|
11
11
|
export function aggregateDefinitions(report: WorkspaceReport): Map<string, DefinitionSite[]> {
|
|
12
12
|
const map = new Map<string, DefinitionSite[]>();
|
|
13
|
-
for (const file of report.files) {
|
|
14
|
-
for (const d of file.definitions) {
|
|
13
|
+
for (const file of report.files ?? []) {
|
|
14
|
+
for (const d of file.definitions ?? []) {
|
|
15
15
|
if (HIDDEN_COMPONENTS.has(d.name)) continue;
|
|
16
16
|
const list = map.get(d.name) ?? [];
|
|
17
17
|
list.push({ kind: d.kind, path: file.path, line: d.line });
|
|
@@ -55,7 +55,7 @@ export function aggregateDeclaredProps(report: WorkspaceReport): Map<string, str
|
|
|
55
55
|
|
|
56
56
|
export function usageMap(report: WorkspaceReport): Map<string, UsageSummary> {
|
|
57
57
|
const m = new Map<string, UsageSummary>();
|
|
58
|
-
for (const row of report.usage_by_component) {
|
|
58
|
+
for (const row of report.usage_by_component ?? []) {
|
|
59
59
|
if (HIDDEN_COMPONENTS.has(row.component)) continue;
|
|
60
60
|
m.set(row.component, row);
|
|
61
61
|
}
|
|
@@ -71,3 +71,16 @@ export function catalogComponentNames(
|
|
|
71
71
|
for (const k of usages.keys()) names.add(k);
|
|
72
72
|
return [...names].sort((a, b) => a.localeCompare(b));
|
|
73
73
|
}
|
|
74
|
+
|
|
75
|
+
/** Stable DOM id for a catalog table row (used for hash deep-links). */
|
|
76
|
+
export function catalogRowDomId(name: string): string {
|
|
77
|
+
return `catalog-row-${encodeURIComponent(name)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Unique component names for sidebar / command palette (definitions ∪ usage). */
|
|
81
|
+
export function componentCatalogNamesFromReport(
|
|
82
|
+
report: WorkspaceReport | null | undefined,
|
|
83
|
+
): string[] {
|
|
84
|
+
if (!report) return [];
|
|
85
|
+
return catalogComponentNames(aggregateDefinitions(report), usageMap(report));
|
|
86
|
+
}
|
|
@@ -52,9 +52,16 @@ export function useWorkspaceReport(
|
|
|
52
52
|
const etagRef = useRef<string | null>(null);
|
|
53
53
|
|
|
54
54
|
// Core fetch function.
|
|
55
|
-
const fetchReport = (
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const fetchReport = (
|
|
56
|
+
url: string,
|
|
57
|
+
cancelled: { value: boolean },
|
|
58
|
+
options?: { showLoading?: boolean },
|
|
59
|
+
) => {
|
|
60
|
+
const showLoading = options?.showLoading !== false;
|
|
61
|
+
if (showLoading) {
|
|
62
|
+
setError(null);
|
|
63
|
+
setLoading(true);
|
|
64
|
+
}
|
|
58
65
|
loadReport(url)
|
|
59
66
|
.then((r) => {
|
|
60
67
|
if (!cancelled.value) setReport(r);
|
|
@@ -63,14 +70,14 @@ export function useWorkspaceReport(
|
|
|
63
70
|
if (!cancelled.value) setError(e instanceof Error ? e.message : "Failed to load report");
|
|
64
71
|
})
|
|
65
72
|
.finally(() => {
|
|
66
|
-
if (!cancelled.value) setLoading(false);
|
|
73
|
+
if (!cancelled.value && showLoading) setLoading(false);
|
|
67
74
|
});
|
|
68
75
|
};
|
|
69
76
|
|
|
70
77
|
// Initial load.
|
|
71
78
|
useEffect(() => {
|
|
72
79
|
const cancelled = { value: false };
|
|
73
|
-
fetchReport(reportUrl, cancelled);
|
|
80
|
+
fetchReport(reportUrl, cancelled, { showLoading: true });
|
|
74
81
|
return () => {
|
|
75
82
|
cancelled.value = true;
|
|
76
83
|
};
|
|
@@ -86,7 +93,7 @@ export function useWorkspaceReport(
|
|
|
86
93
|
source.onmessage = (e) => {
|
|
87
94
|
if (cancelled.value) return;
|
|
88
95
|
if (e.data === "updated") {
|
|
89
|
-
fetchReport(reportUrl, cancelled);
|
|
96
|
+
fetchReport(reportUrl, cancelled, { showLoading: false });
|
|
90
97
|
}
|
|
91
98
|
};
|
|
92
99
|
|
|
@@ -118,7 +125,7 @@ export function useWorkspaceReport(
|
|
|
118
125
|
return;
|
|
119
126
|
}
|
|
120
127
|
etagRef.current = etag;
|
|
121
|
-
fetchReport(reportUrl, cancelled);
|
|
128
|
+
fetchReport(reportUrl, cancelled, { showLoading: false });
|
|
122
129
|
} catch {
|
|
123
130
|
// Network error during poll — ignore silently; the user will see the
|
|
124
131
|
// previous state.
|
|
@@ -10,10 +10,13 @@ export function resolveModuleSourcePath(reportRoot: string, modulePath: string):
|
|
|
10
10
|
return `${root}/${withSrc}`;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const SRC_COMPONENTS = "src/components/";
|
|
14
|
+
|
|
13
15
|
/** Match finding path to source file even when `report.root` differs from machine that generated JSON. */
|
|
14
16
|
function tailSrcComponents(p: string): string | null {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
+
const normalized = norm(p);
|
|
18
|
+
const idx = normalized.indexOf(SRC_COMPONENTS);
|
|
19
|
+
return idx === -1 ? null : normalized.slice(idx);
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export function pathsMatch(reportPath: string, candidate: string): boolean {
|
|
@@ -17,6 +17,7 @@ import { GovernancePane } from "../components/GovernancePane";
|
|
|
17
17
|
import { Sidebar } from "../components/Sidebar";
|
|
18
18
|
import { TokensPane } from "../components/TokensPane";
|
|
19
19
|
import { DashboardCommandPalette } from "../components/DashboardCommandPalette";
|
|
20
|
+
import { componentCatalogNamesFromReport } from "../dashboard/aggregate";
|
|
20
21
|
import { useHashRoute } from "./useHashRoute";
|
|
21
22
|
|
|
22
23
|
const STORAGE_KEY = "dslinter-dashboard-theme";
|
|
@@ -144,8 +145,28 @@ function DashboardLayoutInner({
|
|
|
144
145
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
145
146
|
const { theme, setTheme, resolvedTheme } = useDashboardTheme();
|
|
146
147
|
|
|
148
|
+
const catalogNames = useMemo(
|
|
149
|
+
() => componentCatalogNamesFromReport(dslinterReport.report),
|
|
150
|
+
[dslinterReport.report],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const playgroundIds = useMemo(
|
|
154
|
+
() => new Set(playgroundEntries.map((e) => e.id)),
|
|
155
|
+
[playgroundEntries],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const focusName =
|
|
159
|
+
route.view === "governance" ? route.catalog : undefined;
|
|
160
|
+
|
|
147
161
|
const getEntry = (id: string) => playgroundEntries.find((e) => e.id === id);
|
|
148
162
|
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
if (route.view !== "component") return;
|
|
165
|
+
if (getEntry(route.componentId)) return;
|
|
166
|
+
if (!catalogNames.includes(route.componentId)) return;
|
|
167
|
+
navigate({ view: "governance", catalog: route.componentId });
|
|
168
|
+
}, [route, catalogNames, playgroundEntries, navigate]);
|
|
169
|
+
|
|
149
170
|
let main: ReactNode;
|
|
150
171
|
if (route.view === "tokens") {
|
|
151
172
|
main = <TokensPane tokenCatalog={tokenCatalog} />;
|
|
@@ -156,6 +177,7 @@ function DashboardLayoutInner({
|
|
|
156
177
|
reportUrl={reportUrl}
|
|
157
178
|
dslinterReportHint={dslinterReportHint}
|
|
158
179
|
dslinterReport={dslinterReport}
|
|
180
|
+
focusName={focusName}
|
|
159
181
|
/>
|
|
160
182
|
);
|
|
161
183
|
} else {
|
|
@@ -201,13 +223,17 @@ function DashboardLayoutInner({
|
|
|
201
223
|
)}
|
|
202
224
|
>
|
|
203
225
|
<DashboardCommandPalette
|
|
204
|
-
|
|
226
|
+
catalogNames={catalogNames}
|
|
227
|
+
playgroundIds={playgroundIds}
|
|
205
228
|
onNavigate={navigate}
|
|
206
229
|
open={commandPaletteOpen}
|
|
207
230
|
onOpenChange={setCommandPaletteOpen}
|
|
208
231
|
/>
|
|
209
232
|
<Sidebar
|
|
210
|
-
|
|
233
|
+
report={dslinterReport.report}
|
|
234
|
+
playgroundIds={playgroundIds}
|
|
235
|
+
reportLoading={dslinterReport.loading}
|
|
236
|
+
reportError={dslinterReport.error}
|
|
211
237
|
route={route}
|
|
212
238
|
onNavigate={navigate}
|
|
213
239
|
onOpenCommandPalette={() => setCommandPaletteOpen(true)}
|
package/src/shell/hashRoute.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type HashRoute =
|
|
2
2
|
| { view: "tokens" }
|
|
3
|
-
| { view: "governance" }
|
|
3
|
+
| { view: "governance"; catalog?: string }
|
|
4
4
|
| { view: "component"; componentId: string };
|
|
5
5
|
|
|
6
6
|
const PREFIX = "#!/";
|
|
@@ -26,6 +26,12 @@ export function parseHashRoute(hash: string): HashRoute {
|
|
|
26
26
|
if (raw === "governance") {
|
|
27
27
|
return { view: "governance" };
|
|
28
28
|
}
|
|
29
|
+
if (raw.startsWith("governance/")) {
|
|
30
|
+
const catalog = decodeURIComponent(raw.slice("governance/".length));
|
|
31
|
+
if (catalog.length > 0) {
|
|
32
|
+
return { view: "governance", catalog };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
29
35
|
if (raw.startsWith("component/")) {
|
|
30
36
|
const componentId = decodeURIComponent(raw.slice("component/".length));
|
|
31
37
|
if (componentId.length > 0) {
|
|
@@ -40,7 +46,9 @@ export function formatHashRoute(route: HashRoute): string {
|
|
|
40
46
|
case "tokens":
|
|
41
47
|
return `${PREFIX}tokens`;
|
|
42
48
|
case "governance":
|
|
43
|
-
return
|
|
49
|
+
return route.catalog
|
|
50
|
+
? `${PREFIX}governance/${encodeURIComponent(route.catalog)}`
|
|
51
|
+
: `${PREFIX}governance`;
|
|
44
52
|
case "component":
|
|
45
53
|
return `${PREFIX}component/${encodeURIComponent(route.componentId)}`;
|
|
46
54
|
default:
|