dslinter 0.1.13 → 0.2.2
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 +72 -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 +128 -9
- package/bin/lib/scaffold-config.test.mjs +24 -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 +212 -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 +91 -3
- package/vite/collectScanModules.ts +94 -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
|
@@ -11,6 +11,7 @@ import { shortPath } from "./paths";
|
|
|
11
11
|
import type { LintFinding, Severity } from "../types/report";
|
|
12
12
|
import { Badge } from "../components/ui/badge";
|
|
13
13
|
import { ToggleGroup, ToggleGroupItem } from "../components/ui/toggle-group";
|
|
14
|
+
import { TruncatedPath } from "../components/TruncatedPath";
|
|
14
15
|
|
|
15
16
|
type Filter = "all" | Severity;
|
|
16
17
|
|
|
@@ -42,70 +43,71 @@ export function FindingsList({
|
|
|
42
43
|
return c;
|
|
43
44
|
}, [findings]);
|
|
44
45
|
|
|
45
|
-
const filtered =
|
|
46
|
-
filter === "all" ? findings : findings.filter((f) => f.severity === filter)
|
|
46
|
+
const filtered = useMemo(
|
|
47
|
+
() => (filter === "all" ? findings : findings.filter((f) => f.severity === filter)),
|
|
48
|
+
[findings, filter],
|
|
49
|
+
);
|
|
47
50
|
|
|
48
51
|
if (findings.length === 0) {
|
|
49
52
|
return (
|
|
50
53
|
<p className="rounded-lg border border-dashed border-border bg-muted/30 px-4 py-8 text-center text-sm text-muted-foreground">
|
|
51
|
-
No findings
|
|
54
|
+
No findings
|
|
52
55
|
</p>
|
|
53
56
|
);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
return (
|
|
57
60
|
<div className="space-y-3">
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
<ToggleGroup
|
|
62
|
+
type="single"
|
|
63
|
+
value={filter}
|
|
64
|
+
onValueChange={(value) => {
|
|
65
|
+
if (isFilter(value)) setFilter(value);
|
|
66
|
+
}}
|
|
67
|
+
variant="outline"
|
|
68
|
+
size="sm"
|
|
69
|
+
aria-label="Filter findings by severity"
|
|
70
|
+
className="contents"
|
|
71
|
+
>
|
|
72
|
+
<ToggleGroupItem
|
|
73
|
+
value="all"
|
|
74
|
+
className="rounded-full px-2.5 text-xs font-medium"
|
|
75
|
+
>
|
|
76
|
+
All
|
|
77
|
+
<span className="ml-1 tabular-nums text-muted-foreground">
|
|
78
|
+
{findings.length}
|
|
79
|
+
</span>
|
|
80
|
+
</ToggleGroupItem>
|
|
81
|
+
<ToggleGroupItem
|
|
82
|
+
value="warning"
|
|
83
|
+
className="rounded-full px-2.5 text-xs font-medium"
|
|
84
|
+
>
|
|
85
|
+
Warnings
|
|
86
|
+
<span className="ml-1 tabular-nums text-muted-foreground">
|
|
87
|
+
{counts.warning}
|
|
88
|
+
</span>
|
|
89
|
+
</ToggleGroupItem>
|
|
90
|
+
<ToggleGroupItem
|
|
91
|
+
value="error"
|
|
92
|
+
className="rounded-full px-2.5 text-xs font-medium"
|
|
69
93
|
>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
</ToggleGroupItem>
|
|
88
|
-
<ToggleGroupItem
|
|
89
|
-
value="error"
|
|
90
|
-
className="rounded-full px-2.5 text-xs font-medium"
|
|
91
|
-
>
|
|
92
|
-
Errors
|
|
93
|
-
<span className="ml-1 tabular-nums text-muted-foreground">
|
|
94
|
-
{counts.error}
|
|
95
|
-
</span>
|
|
96
|
-
</ToggleGroupItem>
|
|
97
|
-
<ToggleGroupItem
|
|
98
|
-
value="info"
|
|
99
|
-
className="rounded-full px-2.5 text-xs font-medium"
|
|
100
|
-
>
|
|
101
|
-
Info
|
|
102
|
-
<span className="ml-1 tabular-nums text-muted-foreground">
|
|
103
|
-
{counts.info}
|
|
104
|
-
</span>
|
|
105
|
-
</ToggleGroupItem>
|
|
106
|
-
</ToggleGroup>
|
|
107
|
-
</div>
|
|
108
|
-
<Table>
|
|
94
|
+
Errors
|
|
95
|
+
<span className="ml-1 tabular-nums text-muted-foreground">
|
|
96
|
+
{counts.error}
|
|
97
|
+
</span>
|
|
98
|
+
</ToggleGroupItem>
|
|
99
|
+
<ToggleGroupItem
|
|
100
|
+
value="info"
|
|
101
|
+
className="rounded-full px-2.5 text-xs font-medium"
|
|
102
|
+
>
|
|
103
|
+
Info
|
|
104
|
+
<span className="ml-1 tabular-nums text-muted-foreground">
|
|
105
|
+
{counts.info}
|
|
106
|
+
</span>
|
|
107
|
+
</ToggleGroupItem>
|
|
108
|
+
</ToggleGroup>
|
|
109
|
+
|
|
110
|
+
<Table className="mt-4">
|
|
109
111
|
<TableHeader>
|
|
110
112
|
<TableRow>
|
|
111
113
|
<TableHead scope="col">Severity</TableHead>
|
|
@@ -139,8 +141,16 @@ export function FindingsList({
|
|
|
139
141
|
<TableCell className="whitespace-normal px-3 py-2 text-sm">
|
|
140
142
|
{f.message}
|
|
141
143
|
</TableCell>
|
|
142
|
-
<TableCell className="
|
|
143
|
-
|
|
144
|
+
<TableCell className="min-w-0 px-3 py-2 font-mono text-xs text-muted-foreground">
|
|
145
|
+
<div className="flex min-w-0 items-baseline">
|
|
146
|
+
<TruncatedPath
|
|
147
|
+
path={shortPath(root, f.path)}
|
|
148
|
+
className="min-w-0 flex-1 text-xs"
|
|
149
|
+
/>
|
|
150
|
+
<span className="shrink-0">
|
|
151
|
+
:{f.line != null ? f.line : "—"}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
144
154
|
</TableCell>
|
|
145
155
|
</TableRow>
|
|
146
156
|
))}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
HoverCardTrigger,
|
|
6
6
|
} from "../components/ui/hover-card";
|
|
7
7
|
import { cn } from "../lib/utils";
|
|
8
|
-
import {
|
|
8
|
+
import { TruncatedPath } from "../components/TruncatedPath";
|
|
9
9
|
import {
|
|
10
10
|
filterTokenRows,
|
|
11
11
|
type MergedTokenView,
|
|
@@ -71,8 +71,8 @@ function TokenUsageHover({ row }: { row: ScannedTokenRow }) {
|
|
|
71
71
|
</p>
|
|
72
72
|
<ul className="mt-2 max-h-40 space-y-1 overflow-y-auto text-muted-foreground">
|
|
73
73
|
{row.usageFiles.slice(0, 12).map((f) => (
|
|
74
|
-
<li key={f} className="
|
|
75
|
-
{
|
|
74
|
+
<li key={f} className="min-w-0">
|
|
75
|
+
<TruncatedPath path={f} className="text-xs" />
|
|
76
76
|
</li>
|
|
77
77
|
))}
|
|
78
78
|
{row.usageFiles.length > 12 ? (
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { WorkspaceReport } from "../types/report";
|
|
3
|
+
import { componentCatalogFamiliesFromReport, componentCatalogTreeFromReport } from "./aggregate";
|
|
4
|
+
|
|
5
|
+
function reportWithDefinitions(
|
|
6
|
+
definitions: WorkspaceReport["files"][number]["definitions"],
|
|
7
|
+
path = "/repo/src/components/ui/dropdown-menu.tsx",
|
|
8
|
+
): WorkspaceReport {
|
|
9
|
+
return {
|
|
10
|
+
root: "/repo",
|
|
11
|
+
files: [
|
|
12
|
+
{
|
|
13
|
+
path,
|
|
14
|
+
definitions,
|
|
15
|
+
usages: [],
|
|
16
|
+
parse_errors: [],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
findings: [],
|
|
20
|
+
duplicate_components: [],
|
|
21
|
+
usage_by_component: [],
|
|
22
|
+
scores: {
|
|
23
|
+
design_system_health: 0,
|
|
24
|
+
ux_consistency: 0,
|
|
25
|
+
accessibility: 0,
|
|
26
|
+
maintainability: 0,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("componentCatalogFamiliesFromReport", () => {
|
|
32
|
+
it("groups shadcn-style kebab filename exports under the normalized root", () => {
|
|
33
|
+
const report = reportWithDefinitions([
|
|
34
|
+
{ name: "DropdownMenu", kind: "function", line: 1 },
|
|
35
|
+
{ name: "DropdownMenuContent", kind: "function", line: 2 },
|
|
36
|
+
{ name: "DropdownMenuItem", kind: "function", line: 3 },
|
|
37
|
+
{ name: "DropdownMenuTrigger", kind: "function", line: 4 },
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
expect(componentCatalogFamiliesFromReport(report)).toEqual([
|
|
41
|
+
{
|
|
42
|
+
parent: "DropdownMenu",
|
|
43
|
+
children: ["DropdownMenuContent", "DropdownMenuItem", "DropdownMenuTrigger"],
|
|
44
|
+
path: "/repo/src/components/ui/dropdown-menu.tsx",
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("leaves single export files flat", () => {
|
|
50
|
+
const report = reportWithDefinitions(
|
|
51
|
+
[{ name: "Button", kind: "function", line: 1 }],
|
|
52
|
+
"/repo/src/components/ui/button.tsx",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(componentCatalogFamiliesFromReport(report)).toEqual([]);
|
|
56
|
+
expect(componentCatalogTreeFromReport(report)).toEqual([{ type: "component", name: "Button" }]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("does not group sibling exports when no export matches the filename root", () => {
|
|
60
|
+
const report = reportWithDefinitions(
|
|
61
|
+
[
|
|
62
|
+
{ name: "MenuRoot", kind: "function", line: 1 },
|
|
63
|
+
{ name: "MenuItem", kind: "function", line: 2 },
|
|
64
|
+
],
|
|
65
|
+
"/repo/src/components/ui/dropdown-menu.tsx",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(componentCatalogFamiliesFromReport(report)).toEqual([]);
|
|
69
|
+
expect(componentCatalogTreeFromReport(report)).toEqual([
|
|
70
|
+
{ type: "component", name: "MenuItem" },
|
|
71
|
+
{ type: "component", name: "MenuRoot" },
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type { ComponentDefinition, UsageSummary, WorkspaceReport } from "../types/report";
|
|
1
|
+
import type { ComponentDefinition, FileScan, UsageSummary, WorkspaceReport } from "../types/report";
|
|
2
|
+
import {
|
|
3
|
+
definitionPathsForName,
|
|
4
|
+
isCatalogComponentHidden,
|
|
5
|
+
} from "./catalogVisibility";
|
|
2
6
|
|
|
3
7
|
export interface DefinitionSite {
|
|
4
8
|
kind: ComponentDefinition["kind"];
|
|
@@ -6,13 +10,51 @@ export interface DefinitionSite {
|
|
|
6
10
|
line: number;
|
|
7
11
|
}
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
export type CatalogFamily = {
|
|
14
|
+
parent: string;
|
|
15
|
+
children: string[];
|
|
16
|
+
path: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type CatalogTreeItem = { type: "component"; name: string } | CatalogTreeFamily;
|
|
20
|
+
|
|
21
|
+
export type CatalogTreeFamily = {
|
|
22
|
+
type: "family";
|
|
23
|
+
parent: string;
|
|
24
|
+
children: string[];
|
|
25
|
+
path: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const PLAYABLE_KINDS = new Set<ComponentDefinition["kind"]>([
|
|
29
|
+
"function",
|
|
30
|
+
"class",
|
|
31
|
+
"const_arrow",
|
|
32
|
+
"const_function",
|
|
33
|
+
"wrapped_component",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function isPlayableDefinition(def: ComponentDefinition): boolean {
|
|
37
|
+
return PLAYABLE_KINDS.has(def.kind);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fileStem(path: string): string {
|
|
41
|
+
const base = path.split("/").pop() ?? path;
|
|
42
|
+
return base.replace(/\.(tsx|jsx)$/i, "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizedName(value: string): string {
|
|
46
|
+
return value
|
|
47
|
+
.replace(/\.playground$/i, "")
|
|
48
|
+
.split("")
|
|
49
|
+
.filter((c) => c !== "-" && c !== "_")
|
|
50
|
+
.join("")
|
|
51
|
+
.toLowerCase();
|
|
52
|
+
}
|
|
10
53
|
|
|
11
54
|
export function aggregateDefinitions(report: WorkspaceReport): Map<string, DefinitionSite[]> {
|
|
12
55
|
const map = new Map<string, DefinitionSite[]>();
|
|
13
56
|
for (const file of report.files ?? []) {
|
|
14
57
|
for (const d of file.definitions ?? []) {
|
|
15
|
-
if (HIDDEN_COMPONENTS.has(d.name)) continue;
|
|
16
58
|
const list = map.get(d.name) ?? [];
|
|
17
59
|
list.push({ kind: d.kind, path: file.path, line: d.line });
|
|
18
60
|
map.set(d.name, list);
|
|
@@ -26,19 +68,16 @@ export function aggregateDefinitions(report: WorkspaceReport): Map<string, Defin
|
|
|
26
68
|
|
|
27
69
|
/** Merges `declared_props` from scan definitions and playground rows (source order, then deduped). */
|
|
28
70
|
export function aggregateDeclaredProps(report: WorkspaceReport): Map<string, string[]> {
|
|
29
|
-
const
|
|
71
|
+
const sets = new Map<string, Set<string>>();
|
|
30
72
|
|
|
31
73
|
const add = (name: string, props: readonly string[] | undefined) => {
|
|
32
|
-
if (HIDDEN_COMPONENTS.has(name)) return;
|
|
33
74
|
if (!props?.length) return;
|
|
34
|
-
let
|
|
35
|
-
if (!
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
for (const p of props) {
|
|
40
|
-
if (!list.includes(p)) list.push(p);
|
|
75
|
+
let set = sets.get(name);
|
|
76
|
+
if (!set) {
|
|
77
|
+
set = new Set();
|
|
78
|
+
sets.set(name, set);
|
|
41
79
|
}
|
|
80
|
+
for (const p of props) set.add(p);
|
|
42
81
|
};
|
|
43
82
|
|
|
44
83
|
for (const file of report.files ?? []) {
|
|
@@ -50,37 +89,122 @@ export function aggregateDeclaredProps(report: WorkspaceReport): Map<string, str
|
|
|
50
89
|
add(pg.export_name, pg.declared_props);
|
|
51
90
|
}
|
|
52
91
|
|
|
92
|
+
const map = new Map<string, string[]>();
|
|
93
|
+
for (const [name, set] of sets) {
|
|
94
|
+
map.set(name, [...set]);
|
|
95
|
+
}
|
|
53
96
|
return map;
|
|
54
97
|
}
|
|
55
98
|
|
|
56
99
|
export function usageMap(report: WorkspaceReport): Map<string, UsageSummary> {
|
|
57
100
|
const m = new Map<string, UsageSummary>();
|
|
58
101
|
for (const row of report.usage_by_component ?? []) {
|
|
59
|
-
if (HIDDEN_COMPONENTS.has(row.component)) continue;
|
|
60
102
|
m.set(row.component, row);
|
|
61
103
|
}
|
|
62
104
|
return m;
|
|
63
105
|
}
|
|
64
106
|
|
|
107
|
+
function isVisibleCatalogName(report: WorkspaceReport, name: string): boolean {
|
|
108
|
+
return !isCatalogComponentHidden(
|
|
109
|
+
name,
|
|
110
|
+
report,
|
|
111
|
+
definitionPathsForName(report, name),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
65
115
|
export function catalogComponentNames(
|
|
66
116
|
defs: Map<string, DefinitionSite[]>,
|
|
67
117
|
usages: Map<string, UsageSummary>,
|
|
118
|
+
report: WorkspaceReport,
|
|
68
119
|
): string[] {
|
|
69
120
|
const names = new Set<string>();
|
|
70
|
-
for (const k of defs.keys())
|
|
71
|
-
|
|
121
|
+
for (const k of defs.keys()) {
|
|
122
|
+
if (isVisibleCatalogName(report, k)) names.add(k);
|
|
123
|
+
}
|
|
124
|
+
for (const k of usages.keys()) {
|
|
125
|
+
if (isVisibleCatalogName(report, k)) names.add(k);
|
|
126
|
+
}
|
|
72
127
|
return [...names].sort((a, b) => a.localeCompare(b));
|
|
73
128
|
}
|
|
74
129
|
|
|
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
130
|
/** Unique component names for sidebar / command palette (definitions ∪ usage). */
|
|
81
131
|
export function componentCatalogNamesFromReport(
|
|
82
132
|
report: WorkspaceReport | null | undefined,
|
|
83
133
|
): string[] {
|
|
84
134
|
if (!report) return [];
|
|
85
|
-
return catalogComponentNames(aggregateDefinitions(report), usageMap(report));
|
|
135
|
+
return catalogComponentNames(aggregateDefinitions(report), usageMap(report), report);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function familyFromFile(file: FileScan, report: WorkspaceReport): CatalogFamily | null {
|
|
139
|
+
const defs = (file.definitions ?? []).filter((d) => {
|
|
140
|
+
if (!isPlayableDefinition(d)) return false;
|
|
141
|
+
return !isCatalogComponentHidden(d.name, report, [file.path]);
|
|
142
|
+
});
|
|
143
|
+
if (defs.length < 2) return null;
|
|
144
|
+
|
|
145
|
+
const stem = normalizedName(fileStem(file.path));
|
|
146
|
+
const root = defs.find((d) => normalizedName(d.name) === stem);
|
|
147
|
+
if (!root) return null;
|
|
148
|
+
|
|
149
|
+
const children = defs
|
|
150
|
+
.map((d) => d.name)
|
|
151
|
+
.filter((name) => name !== root.name && name.startsWith(root.name))
|
|
152
|
+
.sort((a, b) => a.localeCompare(b));
|
|
153
|
+
if (children.length === 0) return null;
|
|
154
|
+
|
|
155
|
+
return { parent: root.name, children, path: file.path };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function componentCatalogFamiliesFromReport(
|
|
159
|
+
report: WorkspaceReport | null | undefined,
|
|
160
|
+
): CatalogFamily[] {
|
|
161
|
+
if (!report) return [];
|
|
162
|
+
const byParent = new Map<string, CatalogFamily>();
|
|
163
|
+
for (const file of report.files ?? []) {
|
|
164
|
+
const family = familyFromFile(file, report);
|
|
165
|
+
if (!family) continue;
|
|
166
|
+
if (!isVisibleCatalogName(report, family.parent)) continue;
|
|
167
|
+
const existing = byParent.get(family.parent);
|
|
168
|
+
if (!existing) {
|
|
169
|
+
byParent.set(family.parent, family);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const children = new Set([...existing.children, ...family.children]);
|
|
173
|
+
byParent.set(family.parent, {
|
|
174
|
+
...existing,
|
|
175
|
+
children: [...children].sort((a, b) => a.localeCompare(b)),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return [...byParent.values()].sort((a, b) => a.parent.localeCompare(b.parent));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function componentCatalogTreeFromReport(
|
|
182
|
+
report: WorkspaceReport | null | undefined,
|
|
183
|
+
): CatalogTreeItem[] {
|
|
184
|
+
const names = componentCatalogNamesFromReport(report);
|
|
185
|
+
const families = componentCatalogFamiliesFromReport(report);
|
|
186
|
+
const familyByParent = new Map(families.map((f) => [f.parent, f]));
|
|
187
|
+
const childNames = new Set(families.flatMap((f) => f.children));
|
|
188
|
+
const items: CatalogTreeItem[] = [];
|
|
189
|
+
|
|
190
|
+
for (const name of names) {
|
|
191
|
+
const family = familyByParent.get(name);
|
|
192
|
+
if (family) {
|
|
193
|
+
items.push({ type: "family", ...family });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (childNames.has(name)) continue;
|
|
197
|
+
items.push({ type: "component", name });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return items;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function componentCatalogFamilyForName(
|
|
204
|
+
report: WorkspaceReport | null | undefined,
|
|
205
|
+
name: string,
|
|
206
|
+
): CatalogFamily | undefined {
|
|
207
|
+
return componentCatalogFamiliesFromReport(report).find(
|
|
208
|
+
(family) => family.parent === name || family.children.includes(name),
|
|
209
|
+
);
|
|
86
210
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { WorkspaceReport } from "../types/report";
|
|
3
|
+
import {
|
|
4
|
+
componentCatalogNamesFromReport,
|
|
5
|
+
componentCatalogTreeFromReport,
|
|
6
|
+
} from "./aggregate";
|
|
7
|
+
import { isCatalogComponentHidden, pathMatchesPrefix, reportWithExtraHidden } from "./catalogVisibility";
|
|
8
|
+
|
|
9
|
+
function minimalReport(overrides: Partial<WorkspaceReport> = {}): WorkspaceReport {
|
|
10
|
+
return {
|
|
11
|
+
root: "/repo",
|
|
12
|
+
files: [],
|
|
13
|
+
findings: [],
|
|
14
|
+
duplicate_components: [],
|
|
15
|
+
usage_by_component: [],
|
|
16
|
+
scores: {
|
|
17
|
+
design_system_health: 0,
|
|
18
|
+
ux_consistency: 0,
|
|
19
|
+
accessibility: 0,
|
|
20
|
+
maintainability: 0,
|
|
21
|
+
},
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("catalogVisibility", () => {
|
|
27
|
+
it("pathMatchesPrefix treats repo-relative paths", () => {
|
|
28
|
+
expect(pathMatchesPrefix("resources/js/components/ui/button.tsx", "resources/js/components")).toBe(
|
|
29
|
+
true,
|
|
30
|
+
);
|
|
31
|
+
expect(pathMatchesPrefix("resources/js/pages/foo.tsx", "resources/js/components")).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("hides by component name from report config", () => {
|
|
35
|
+
const report = minimalReport({
|
|
36
|
+
config: { hidden_components: ["Secret"] },
|
|
37
|
+
files: [
|
|
38
|
+
{
|
|
39
|
+
path: "resources/js/components/secret.tsx",
|
|
40
|
+
definitions: [{ name: "Secret", kind: "function", line: 1 }],
|
|
41
|
+
usages: [],
|
|
42
|
+
parse_errors: [],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
path: "resources/js/components/button.tsx",
|
|
46
|
+
definitions: [{ name: "Button", kind: "function", line: 1 }],
|
|
47
|
+
usages: [],
|
|
48
|
+
parse_errors: [],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
expect(isCatalogComponentHidden("Secret", report, ["resources/js/components/secret.tsx"])).toBe(
|
|
53
|
+
true,
|
|
54
|
+
);
|
|
55
|
+
expect(componentCatalogNamesFromReport(report)).toEqual(["Button"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("hides by path prefix", () => {
|
|
59
|
+
const report = minimalReport({
|
|
60
|
+
config: { hidden_paths: ["resources/js/components/legacy"] },
|
|
61
|
+
files: [
|
|
62
|
+
{
|
|
63
|
+
path: "resources/js/components/legacy/old.tsx",
|
|
64
|
+
definitions: [{ name: "Old", kind: "function", line: 1 }],
|
|
65
|
+
usages: [],
|
|
66
|
+
parse_errors: [],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
expect(componentCatalogNamesFromReport(report)).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("reportWithExtraHidden merges optimistic names", () => {
|
|
74
|
+
const report = minimalReport({
|
|
75
|
+
files: [
|
|
76
|
+
{
|
|
77
|
+
path: "a.tsx",
|
|
78
|
+
definitions: [{ name: "A", kind: "function", line: 1 }],
|
|
79
|
+
usages: [],
|
|
80
|
+
parse_errors: [],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
path: "b.tsx",
|
|
84
|
+
definitions: [{ name: "B", kind: "function", line: 1 }],
|
|
85
|
+
usages: [],
|
|
86
|
+
parse_errors: [],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
const merged = reportWithExtraHidden(report, ["B"]);
|
|
91
|
+
expect(componentCatalogTreeFromReport(merged)).toEqual([{ type: "component", name: "A" }]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { WorkspaceReport } from "../types/report";
|
|
2
|
+
|
|
3
|
+
export type ReportConfig = NonNullable<WorkspaceReport["config"]>;
|
|
4
|
+
|
|
5
|
+
const BUILTIN_HIDDEN = new Set(["App", "React.StrictMode"]);
|
|
6
|
+
|
|
7
|
+
function trimLeadingSlashes(value: string): string {
|
|
8
|
+
let i = 0;
|
|
9
|
+
while (i < value.length && value.charCodeAt(i) === 47) i += 1;
|
|
10
|
+
return value.slice(i);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function trimTrailingSlashes(value: string): string {
|
|
14
|
+
let end = value.length;
|
|
15
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
16
|
+
return value.slice(0, end);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function normalizePath(path: string): string {
|
|
20
|
+
return trimLeadingSlashes(path.replaceAll("\\", "/"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizePrefix(prefix: string): string {
|
|
24
|
+
return trimTrailingSlashes(trimLeadingSlashes(prefix.trim()));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function pathMatchesPrefix(rel: string, prefix: string): boolean {
|
|
28
|
+
const pre = normalizePrefix(prefix);
|
|
29
|
+
if (!pre) return false;
|
|
30
|
+
const r = normalizePath(rel);
|
|
31
|
+
return r === pre || r.startsWith(`${pre}/`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function reportConfig(
|
|
35
|
+
report: WorkspaceReport | null | undefined,
|
|
36
|
+
): ReportConfig {
|
|
37
|
+
return report?.config ?? {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hiddenComponentNames(
|
|
41
|
+
report: WorkspaceReport | null | undefined,
|
|
42
|
+
extraHidden?: Iterable<string>,
|
|
43
|
+
): Set<string> {
|
|
44
|
+
const names = new Set(BUILTIN_HIDDEN);
|
|
45
|
+
for (const n of reportConfig(report).hidden_components ?? []) {
|
|
46
|
+
names.add(n);
|
|
47
|
+
}
|
|
48
|
+
if (extraHidden) {
|
|
49
|
+
for (const n of extraHidden) names.add(n);
|
|
50
|
+
}
|
|
51
|
+
return names;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hiddenPathPrefixes(
|
|
55
|
+
report: WorkspaceReport | null | undefined,
|
|
56
|
+
): string[] {
|
|
57
|
+
return (reportConfig(report).hidden_paths ?? []).map(normalizePrefix).filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when the component should not appear in sidebar / palette / playground list. */
|
|
61
|
+
export function isCatalogComponentHidden(
|
|
62
|
+
name: string,
|
|
63
|
+
report: WorkspaceReport | null | undefined,
|
|
64
|
+
definitionPaths: string[] | undefined,
|
|
65
|
+
extraHidden?: Iterable<string>,
|
|
66
|
+
): boolean {
|
|
67
|
+
if (hiddenComponentNames(report, extraHidden).has(name)) return true;
|
|
68
|
+
const prefixes = hiddenPathPrefixes(report);
|
|
69
|
+
if (!prefixes.length || !definitionPaths?.length) return false;
|
|
70
|
+
for (const path of definitionPaths) {
|
|
71
|
+
const rel = normalizePath(path);
|
|
72
|
+
for (const pre of prefixes) {
|
|
73
|
+
if (pathMatchesPrefix(rel, pre)) return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function definitionPathsForName(
|
|
80
|
+
report: WorkspaceReport | null | undefined,
|
|
81
|
+
name: string,
|
|
82
|
+
): string[] {
|
|
83
|
+
if (!report) return [];
|
|
84
|
+
const paths: string[] = [];
|
|
85
|
+
for (const file of report.files ?? []) {
|
|
86
|
+
for (const d of file.definitions ?? []) {
|
|
87
|
+
if (d.name === name) paths.push(file.path);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return paths;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Merge optimistic hides until the next report refresh includes them. */
|
|
94
|
+
export function reportWithExtraHidden(
|
|
95
|
+
report: WorkspaceReport | null | undefined,
|
|
96
|
+
extraHidden: readonly string[],
|
|
97
|
+
): WorkspaceReport | null | undefined {
|
|
98
|
+
if (!report || extraHidden.length === 0) return report;
|
|
99
|
+
const existing = report.config?.hidden_components ?? [];
|
|
100
|
+
const merged = [...new Set([...existing, ...extraHidden])];
|
|
101
|
+
return {
|
|
102
|
+
...report,
|
|
103
|
+
config: {
|
|
104
|
+
...report.config,
|
|
105
|
+
hidden_components: merged,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|