dslinter 0.2.3 → 0.3.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 +17 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +14 -1
- package/bin/lib/infer-prop-types-from-ts.test.mjs +32 -0
- package/dashboard-dist/assets/{DashboardLayoutAuto-h0gP_iKd.js → DashboardLayoutAuto-B4P-sy4z.js} +1 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +1 -0
- package/dashboard-dist/assets/{axe-DDaE9JTN.js → axe-CaxTXfM9.js} +1 -1
- package/dashboard-dist/assets/index-B432JkIx.js +219 -0
- package/dashboard-dist/assets/index-D0O_5w5V.css +1 -0
- package/dashboard-dist/dslinter-report.json +23929 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +52 -52
- package/package.json +6 -6
- package/src/components/CatalogPane.tsx +94 -0
- package/src/components/ComponentInspectPane.tsx +125 -125
- package/src/components/ComponentPlaygroundPane.tsx +42 -27
- package/src/components/DashboardCommandPalette.tsx +9 -0
- package/src/components/GovernanceInventoryTabs.tsx +51 -0
- package/src/components/GovernancePane.tsx +18 -5
- package/src/components/PlaygroundA11yAndCode.tsx +0 -52
- package/src/components/PlaygroundControlField.tsx +2 -0
- package/src/components/ScoreGauge.test.ts +22 -0
- package/src/components/ScoreGauge.tsx +179 -0
- package/src/components/Sidebar.tsx +97 -23
- package/src/components/TokensPane.tsx +11 -13
- package/src/components/controlApiTable.test.ts +15 -0
- package/src/components/controlApiTable.ts +4 -0
- package/src/components/ui/badge.tsx +5 -5
- package/src/dashboard/ComponentCatalog.tsx +10 -1
- package/src/dashboard/ComponentPropUsageDetail.tsx +127 -42
- package/src/dashboard/ComponentUsageDetails.tsx +39 -9
- package/src/dashboard/DashboardBody.tsx +83 -12
- package/src/dashboard/ScannedTokenWall.tsx +9 -6
- package/src/dashboard/UnusedComponentsList.tsx +74 -0
- package/src/dashboard/aggregate.test.ts +381 -12
- package/src/dashboard/aggregate.ts +167 -30
- package/src/dashboard/mergeTokenCatalog.ts +5 -0
- package/src/dashboard/paths.test.ts +18 -1
- package/src/dashboard/paths.ts +8 -0
- package/src/mcp/agent-query.ts +1 -1
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +3 -3
- package/src/playground/controls.ts +16 -3
- package/src/playground/enrichKitControls.ts +5 -5
- package/src/playground/inferKitJsx.test.ts +0 -11
- package/src/playground/inferPropTypesFromTs.d.mts +1 -1
- package/src/playground/inferPropTypesFromTs.mjs +19 -3
- package/src/playground/inferPropTypesFromTs.test.ts +32 -0
- package/src/playground/inferPropTypesFromTs.ts +1 -1
- package/src/playground/playgroundJoin.ts +34 -0
- package/src/playground/propCoerce.ts +2 -2
- package/src/playground/snippet.ts +1 -0
- package/src/shell/DashboardLayout.tsx +21 -4
- package/src/shell/hashRoute.test.ts +9 -0
- package/src/shell/hashRoute.ts +6 -0
- package/src/types/controls.ts +12 -0
- package/src/types/report.ts +1 -1
- package/vite/embedTailwindSources.ts +8 -6
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +0 -1
- package/dashboard-dist/assets/index-B9sZ6wHm.css +0 -1
- package/dashboard-dist/assets/index-DIDBt5ed.js +0 -218
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { truncatePathMiddle } from "./paths";
|
|
2
|
+
import { resolveReportAbsolutePath, truncatePathMiddle } from "./paths";
|
|
3
3
|
|
|
4
4
|
const SAMPLE =
|
|
5
5
|
"resources/js/components/manage-two-factor.tsx";
|
|
6
6
|
|
|
7
|
+
describe("resolveReportAbsolutePath", () => {
|
|
8
|
+
it("returns absolute paths unchanged when under root", () => {
|
|
9
|
+
expect(
|
|
10
|
+
resolveReportAbsolutePath(
|
|
11
|
+
"/repo",
|
|
12
|
+
"/repo/src/components/Button.tsx",
|
|
13
|
+
),
|
|
14
|
+
).toBe("/repo/src/components/Button.tsx");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("joins root-relative paths", () => {
|
|
18
|
+
expect(
|
|
19
|
+
resolveReportAbsolutePath("/repo", "src/pages/Home.tsx"),
|
|
20
|
+
).toBe("/repo/src/pages/Home.tsx");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
7
24
|
describe("truncatePathMiddle", () => {
|
|
8
25
|
it("returns the path unchanged when it fits", () => {
|
|
9
26
|
expect(truncatePathMiddle(SAMPLE, 100)).toBe(SAMPLE);
|
package/src/dashboard/paths.ts
CHANGED
|
@@ -14,6 +14,14 @@ export function shortPath(root: string, fullPath: string): string {
|
|
|
14
14
|
return parts.slice(-3).join("/");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/** Resolve a report path (absolute or root-relative) to an absolute path. */
|
|
18
|
+
export function resolveReportAbsolutePath(root: string, path: string): string {
|
|
19
|
+
const r = normalizePath(root);
|
|
20
|
+
const p = normalizePath(path);
|
|
21
|
+
if (p.startsWith(r + "/") || p === r) return p;
|
|
22
|
+
return `${r}/${p}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* Truncate a file path from the middle, preserving the filename and path
|
|
19
27
|
* separators. Leading path segments are kept when space allows.
|
package/src/mcp/agent-query.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { findingsForComponent } from "../report/findingsForComponent";
|
|
8
8
|
import { controlsForSpec } from "../playground/controls";
|
|
9
9
|
import { genericUsageSnippet } from "../playground/snippet";
|
|
10
|
-
import { pillarForRule,
|
|
10
|
+
import { pillarForRule, ruleCatalog } from "./rule-catalog";
|
|
11
11
|
import { findingMatchesPath } from "./normalize-paths";
|
|
12
12
|
import type {
|
|
13
13
|
ConfigSnapshot,
|
|
@@ -86,7 +86,7 @@ describe("playground preview props", () => {
|
|
|
86
86
|
it("maps playground control values even when declared_props omits them", () => {
|
|
87
87
|
const controlOverrides: Record<string, PlaygroundControl[]> = {
|
|
88
88
|
Demo: [
|
|
89
|
-
{ key: "children", label: "children", type: "
|
|
89
|
+
{ key: "children", label: "children", type: "node", default: "" },
|
|
90
90
|
{
|
|
91
91
|
key: "variant",
|
|
92
92
|
label: "variant",
|
|
@@ -168,8 +168,8 @@ describe("playground preview props", () => {
|
|
|
168
168
|
expect(asChild?.type).toBe("boolean");
|
|
169
169
|
|
|
170
170
|
const children = entry!.controls.find((c) => c.key === "children");
|
|
171
|
-
expect(children?.type).toBe("
|
|
172
|
-
if (children?.type === "
|
|
171
|
+
expect(children?.type).toBe("node");
|
|
172
|
+
if (children?.type === "node") {
|
|
173
173
|
expect(children.default).toBe("Example");
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -19,18 +19,19 @@ export function stringDefaultForProp(key: string): string {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export type PlaygroundStringControl = Extract<PlaygroundControl, { type: "string" }>;
|
|
22
|
+
export type PlaygroundNodeControl = Extract<PlaygroundControl, { type: "node" }>;
|
|
22
23
|
|
|
23
24
|
/** Parts like BreadcrumbSeparator default to an icon when children is omitted. */
|
|
24
25
|
export function usesIconChildrenFallback(exportName: string): boolean {
|
|
25
26
|
return exportName.endsWith("Separator");
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function childrenControl(exportName?: string):
|
|
29
|
+
export function childrenControl(exportName?: string): PlaygroundNodeControl {
|
|
29
30
|
if (exportName && usesIconChildrenFallback(exportName)) {
|
|
30
31
|
return {
|
|
31
32
|
key: "children",
|
|
32
33
|
label: "children",
|
|
33
|
-
type: "
|
|
34
|
+
type: "node",
|
|
34
35
|
default: "",
|
|
35
36
|
placeholder: "Custom separator (chevron when empty)",
|
|
36
37
|
};
|
|
@@ -38,12 +39,22 @@ export function childrenControl(exportName?: string): PlaygroundStringControl {
|
|
|
38
39
|
return {
|
|
39
40
|
key: "children",
|
|
40
41
|
label: "children",
|
|
41
|
-
type: "
|
|
42
|
+
type: "node",
|
|
42
43
|
default: CHILDREN_SLOT_DEFAULT,
|
|
43
44
|
placeholder: "Slot content",
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
|
|
48
|
+
export function nodeControlForProp(key: string): PlaygroundNodeControl {
|
|
49
|
+
return {
|
|
50
|
+
key,
|
|
51
|
+
label: key,
|
|
52
|
+
type: "node",
|
|
53
|
+
default: defaultStringForProp(key),
|
|
54
|
+
placeholder: "Slot content",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
export function childrenPropForPreview(
|
|
48
59
|
exportName: string | undefined,
|
|
49
60
|
raw: unknown,
|
|
@@ -133,6 +144,8 @@ export function controlsFromDeclaredProps(
|
|
|
133
144
|
out.push({ key, label: key, type: "boolean", default: false });
|
|
134
145
|
} else if (kind === "number") {
|
|
135
146
|
out.push({ key, label: key, type: "number", default: 0 });
|
|
147
|
+
} else if (kind === "node") {
|
|
148
|
+
out.push(nodeControlForProp(key));
|
|
136
149
|
} else if (kind === "string") {
|
|
137
150
|
out.push({
|
|
138
151
|
key,
|
|
@@ -29,7 +29,7 @@ function enrichOneControl(control: PlaygroundControl, slot: KitJsxSlot | undefin
|
|
|
29
29
|
const exampleDefault = slotDefaultFromComponent(slot.component);
|
|
30
30
|
const hint = `${slot.component} children`;
|
|
31
31
|
|
|
32
|
-
if (control.type === "string") {
|
|
32
|
+
if (control.type === "string" || control.type === "node") {
|
|
33
33
|
const useExample =
|
|
34
34
|
exampleDefault !== undefined &&
|
|
35
35
|
(control.default === control.key || control.default === control.key.toLowerCase());
|
|
@@ -102,12 +102,12 @@ function propMetadataForCatalog(
|
|
|
102
102
|
|
|
103
103
|
const def = definitionForExport(report, spec.export_name);
|
|
104
104
|
const propOptions = {
|
|
105
|
-
...
|
|
106
|
-
...
|
|
105
|
+
...def?.declared_prop_options,
|
|
106
|
+
...spec.declared_prop_options,
|
|
107
107
|
};
|
|
108
108
|
const propDefaults = {
|
|
109
|
-
...
|
|
110
|
-
...
|
|
109
|
+
...def?.declared_prop_defaults,
|
|
110
|
+
...spec.declared_prop_defaults,
|
|
111
111
|
};
|
|
112
112
|
const propKinds = { ...spec.declared_prop_kinds };
|
|
113
113
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createElement } from "react";
|
|
2
1
|
import { describe, expect, it } from "vitest";
|
|
3
2
|
import {
|
|
4
3
|
inferKitJsxSlots,
|
|
@@ -7,16 +6,6 @@ import {
|
|
|
7
6
|
slotLabelFromComponent,
|
|
8
7
|
} from "./inferKitJsx";
|
|
9
8
|
|
|
10
|
-
function Alert(props: { variant?: string; children?: unknown }) {
|
|
11
|
-
return createElement("div", props);
|
|
12
|
-
}
|
|
13
|
-
function AlertTitle(props: { children?: unknown }) {
|
|
14
|
-
return createElement("div", props);
|
|
15
|
-
}
|
|
16
|
-
function AlertDescription(props: { children?: unknown }) {
|
|
17
|
-
return createElement("div", props);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
9
|
describe("inferKitJsx", () => {
|
|
21
10
|
it("reads createElement children bindings (including vite transforms)", () => {
|
|
22
11
|
const viteTransformed =
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Program, TypeChecker, SourceFile, Type } from "typescript";
|
|
2
2
|
import type { PlaygroundSpec } from "../types/report.js";
|
|
3
3
|
|
|
4
|
-
export type PropKind = "boolean" | "string" | "number";
|
|
4
|
+
export type PropKind = "boolean" | "string" | "number" | "node";
|
|
5
5
|
|
|
6
6
|
export type CheckerProgram = {
|
|
7
7
|
program: Program;
|
|
@@ -122,12 +122,22 @@ export function findComponentParamType(checker, sf, exportName) {
|
|
|
122
122
|
return found;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** @param {ts.TypeChecker} checker @param {ts.Type} type */
|
|
126
|
+
function isReactNodeType(checker, type) {
|
|
127
|
+
const alias = type.aliasSymbol?.escapedName ?? type.aliasSymbol?.name;
|
|
128
|
+
if (alias === "ReactNode" || alias === "ReactElement") return true;
|
|
129
|
+
const text = checker.typeToString(type);
|
|
130
|
+
if (text === "ReactNode" || text === "ReactElement") return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
125
134
|
/**
|
|
126
135
|
* @param {ts.TypeChecker} checker
|
|
127
136
|
* @param {ts.Type} type
|
|
128
137
|
* @returns {PropKind | null}
|
|
129
138
|
*/
|
|
130
139
|
export function classifyPropType(checker, type) {
|
|
140
|
+
if (isReactNodeType(checker, type)) return "node";
|
|
131
141
|
const nn = checker.getNonNullableType(type);
|
|
132
142
|
if (nn.isUnion()) {
|
|
133
143
|
const parts = nn.types.map((u) =>
|
|
@@ -146,10 +156,16 @@ export function classifyPropType(checker, type) {
|
|
|
146
156
|
return null;
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
/** @param {ts.TypeChecker} checker @param {ts.Type} type */
|
|
150
|
-
function followTypeAlias(checker, type) {
|
|
159
|
+
/** @param {ts.TypeChecker} checker @param {ts.Type} type @param {Set<number>} [seen] */
|
|
160
|
+
function followTypeAlias(checker, type, seen = new Set()) {
|
|
151
161
|
if (type.aliasSymbol) {
|
|
152
|
-
|
|
162
|
+
if (seen.has(type.id)) return type;
|
|
163
|
+
seen.add(type.id);
|
|
164
|
+
return followTypeAlias(
|
|
165
|
+
checker,
|
|
166
|
+
checker.getDeclaredTypeOfSymbol(type.aliasSymbol),
|
|
167
|
+
seen,
|
|
168
|
+
);
|
|
153
169
|
}
|
|
154
170
|
return type;
|
|
155
171
|
}
|
|
@@ -217,6 +217,38 @@ describe("enrichPlaygroundSpecFromTs", () => {
|
|
|
217
217
|
expect(enriched.declared_prop_defaults?.type).toBe("text");
|
|
218
218
|
expect(enriched.declared_prop_kinds?.type).toBe("string");
|
|
219
219
|
});
|
|
220
|
+
|
|
221
|
+
it("classifies ReactNode props as node", () => {
|
|
222
|
+
const root = tempProject({
|
|
223
|
+
"src/section.tsx": `
|
|
224
|
+
import type { ReactNode } from "react";
|
|
225
|
+
export function Section({
|
|
226
|
+
children,
|
|
227
|
+
actions,
|
|
228
|
+
}: {
|
|
229
|
+
children: ReactNode;
|
|
230
|
+
actions?: ReactNode;
|
|
231
|
+
}) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
`,
|
|
235
|
+
});
|
|
236
|
+
const bundle = createCheckerProgram(root)!;
|
|
237
|
+
const spec: PlaygroundSpec = {
|
|
238
|
+
id: "Section",
|
|
239
|
+
export_name: "Section",
|
|
240
|
+
rel_path: "src/section.tsx",
|
|
241
|
+
declared_props: ["children", "actions"],
|
|
242
|
+
};
|
|
243
|
+
const enriched = enrichPlaygroundSpecFromTs(
|
|
244
|
+
spec,
|
|
245
|
+
bundle.checker,
|
|
246
|
+
bundle.program,
|
|
247
|
+
root,
|
|
248
|
+
);
|
|
249
|
+
expect(enriched.declared_prop_kinds?.children).toBe("node");
|
|
250
|
+
expect(enriched.declared_prop_kinds?.actions).toBe("node");
|
|
251
|
+
});
|
|
220
252
|
});
|
|
221
253
|
|
|
222
254
|
describe("createCheckerProgram", () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { DeclaredPropKind, PlaygroundSpec } from "../types/report";
|
|
2
|
-
export type PropKind = "boolean" | "string" | "number";
|
|
2
|
+
export type PropKind = "boolean" | "string" | "number" | "node";
|
|
3
3
|
|
|
4
4
|
export type CheckerProgram = {
|
|
5
5
|
program: import("typescript").Program;
|
|
@@ -168,3 +168,37 @@ export function findPlaygroundJoinSkip(
|
|
|
168
168
|
): PlaygroundJoinSkip | undefined {
|
|
169
169
|
return skipped?.find((s) => s.export_name === componentId);
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
/** User-facing hint when a playground spec cannot join to the Vite module graph. */
|
|
173
|
+
export function playgroundJoinDetailMessage(
|
|
174
|
+
skip: PlaygroundJoinSkip | undefined,
|
|
175
|
+
spec: PlaygroundSpec | undefined,
|
|
176
|
+
): string | null {
|
|
177
|
+
if (skip?.reason === "module_not_found") {
|
|
178
|
+
const { globKey, rel_path } = skip;
|
|
179
|
+
const subdirHint = !rel_path.includes("/")
|
|
180
|
+
? " This usually means the scanner was run from a subdirectory. Re-run from the project root: npx dslinter ."
|
|
181
|
+
: "";
|
|
182
|
+
if (globKey.startsWith("@dslinter-scan/")) {
|
|
183
|
+
return [
|
|
184
|
+
`Expected module key "${globKey}" but the dslinter Vite plugin did not load it.`,
|
|
185
|
+
`Use <DashboardLayout autoPlayground /> and run via npx dslinter (zero vite.config changes), or add plugins: [dslinter()] from dslinter/vite to vite.config.ts.`,
|
|
186
|
+
`Run the scanner from the project root so rel_path "${rel_path}" matches files under DSLINTER_SCAN_ROOT.`,
|
|
187
|
+
].join(" ");
|
|
188
|
+
}
|
|
189
|
+
return [
|
|
190
|
+
`Vite glob is missing key "${globKey}" for report path "${rel_path}".`,
|
|
191
|
+
`Prefer <DashboardLayout autoPlayground /> with plugins: [dslinter()] from dslinter/vite, or run npx dslinter init for a custom buildRegistry.ts glob.`,
|
|
192
|
+
subdirHint,
|
|
193
|
+
]
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.join("");
|
|
196
|
+
}
|
|
197
|
+
if (skip?.reason === "export_not_found") {
|
|
198
|
+
return `Module loaded but named export "${skip.export_name}" was not found. Use export function ${skip.export_name}(…) in ${skip.rel_path}.`;
|
|
199
|
+
}
|
|
200
|
+
if (spec) {
|
|
201
|
+
return `Report path: ${spec.rel_path} (export ${spec.export_name}). Use autoPlayground with dslinter/vite, or ensure buildRegistry.ts glob covers this file.`;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./controls";
|
|
9
9
|
|
|
10
10
|
export function coerceDeclaredPropKind(v: unknown): DeclaredPropKind | undefined {
|
|
11
|
-
if (v === "boolean" || v === "string" || v === "number" || v === "unknown")
|
|
11
|
+
if (v === "boolean" || v === "string" || v === "number" || v === "node" || v === "unknown")
|
|
12
12
|
return v;
|
|
13
13
|
return undefined;
|
|
14
14
|
}
|
|
@@ -65,7 +65,7 @@ export function valuesToComponentProps(
|
|
|
65
65
|
o[key] = Number.isFinite(n) ? n : 0;
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
68
|
-
if (kind === "string") {
|
|
68
|
+
if (kind === "string" || kind === "node") {
|
|
69
69
|
o[key] = values[key];
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
@@ -14,6 +14,7 @@ import type { TokenCatalog } from "../types/tokenCatalog";
|
|
|
14
14
|
import type { DslinterReportState } from "../dashboard/useWorkspaceReport";
|
|
15
15
|
import { ComponentInspectPane } from "../components/ComponentInspectPane";
|
|
16
16
|
import { ComponentPlaygroundPane } from "../components/ComponentPlaygroundPane";
|
|
17
|
+
import { CatalogPane } from "../components/CatalogPane";
|
|
17
18
|
import { GovernancePane } from "../components/GovernancePane";
|
|
18
19
|
import { Sidebar } from "../components/Sidebar";
|
|
19
20
|
import { TokensPane } from "../components/TokensPane";
|
|
@@ -21,6 +22,7 @@ import { DashboardCommandPalette } from "../components/DashboardCommandPalette";
|
|
|
21
22
|
import {
|
|
22
23
|
componentCatalogNamesFromReport,
|
|
23
24
|
componentCatalogTreeFromReport,
|
|
25
|
+
resolveFamilyNavigationTarget,
|
|
24
26
|
} from "../dashboard/aggregate";
|
|
25
27
|
import { reportWithExtraHidden } from "../dashboard/catalogVisibility";
|
|
26
28
|
import { resolvePlaygroundEntry } from "../playground/buildPlaygroundEntriesFromReport";
|
|
@@ -186,14 +188,17 @@ export function DashboardLayoutInner({
|
|
|
186
188
|
return [{ name: item.name, label: item.name }];
|
|
187
189
|
}
|
|
188
190
|
return [
|
|
189
|
-
{
|
|
191
|
+
{
|
|
192
|
+
name: resolveFamilyNavigationTarget(item, catalogNames),
|
|
193
|
+
label: item.parent,
|
|
194
|
+
},
|
|
190
195
|
...item.children.map((child) => ({
|
|
191
196
|
name: child,
|
|
192
197
|
label: `${item.parent} / ${child}`,
|
|
193
198
|
})),
|
|
194
199
|
];
|
|
195
200
|
});
|
|
196
|
-
}, [catalogReport]);
|
|
201
|
+
}, [catalogReport, catalogNames]);
|
|
197
202
|
|
|
198
203
|
const handleHideFromCatalog = useCallback(
|
|
199
204
|
(componentName: string) => {
|
|
@@ -230,9 +235,22 @@ export function DashboardLayoutInner({
|
|
|
230
235
|
onOpenComponent={(name) =>
|
|
231
236
|
navigate({ view: "component", componentId: name })
|
|
232
237
|
}
|
|
238
|
+
onOpenCatalog={() => navigate({ view: "catalog" })}
|
|
233
239
|
/>
|
|
234
240
|
);
|
|
235
|
-
} else {
|
|
241
|
+
} else if (route.view === "catalog") {
|
|
242
|
+
main = (
|
|
243
|
+
<CatalogPane
|
|
244
|
+
landing={overview}
|
|
245
|
+
dslinterReportHint={dslinterReportHint}
|
|
246
|
+
dslinterReport={dslinterReport}
|
|
247
|
+
onOpenComponent={(name) =>
|
|
248
|
+
navigate({ view: "component", componentId: name })
|
|
249
|
+
}
|
|
250
|
+
onBackToGovernance={() => navigate({ view: "governance" })}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
} else if (route.view === "component") {
|
|
236
254
|
const componentId = route.componentId;
|
|
237
255
|
const entry = resolvePlaygroundEntry(playgroundEntries, componentId);
|
|
238
256
|
const inCatalog = catalogNames.includes(componentId);
|
|
@@ -264,7 +282,6 @@ export function DashboardLayoutInner({
|
|
|
264
282
|
playgroundJoinSkips,
|
|
265
283
|
componentId,
|
|
266
284
|
)}
|
|
267
|
-
onBackToGovernance={() => navigate({ view: "governance" })}
|
|
268
285
|
onOpenComponent={(name) =>
|
|
269
286
|
navigate({ view: "component", componentId: name })
|
|
270
287
|
}
|
|
@@ -18,6 +18,11 @@ describe("parseHashRoute", () => {
|
|
|
18
18
|
it("parses tokens", () => {
|
|
19
19
|
expect(parseHashRoute("/tokens")).toEqual({ view: "tokens" });
|
|
20
20
|
});
|
|
21
|
+
|
|
22
|
+
it("parses catalog", () => {
|
|
23
|
+
expect(parseHashRoute("/catalog")).toEqual({ view: "catalog" });
|
|
24
|
+
expect(parseHashRoute("/governance/catalog")).toEqual({ view: "catalog" });
|
|
25
|
+
});
|
|
21
26
|
});
|
|
22
27
|
|
|
23
28
|
describe("formatHashRoute", () => {
|
|
@@ -30,4 +35,8 @@ describe("formatHashRoute", () => {
|
|
|
30
35
|
it("formats governance", () => {
|
|
31
36
|
expect(formatHashRoute({ view: "governance" })).toBe("/governance");
|
|
32
37
|
});
|
|
38
|
+
|
|
39
|
+
it("formats catalog", () => {
|
|
40
|
+
expect(formatHashRoute({ view: "catalog" })).toBe("/catalog");
|
|
41
|
+
});
|
|
33
42
|
});
|
package/src/shell/hashRoute.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type HashRoute =
|
|
2
2
|
| { view: "tokens" }
|
|
3
3
|
| { view: "governance" }
|
|
4
|
+
| { view: "catalog" }
|
|
4
5
|
| { view: "component"; componentId: string };
|
|
5
6
|
|
|
6
7
|
function trimTrailingSlashes(path: string): string {
|
|
@@ -27,6 +28,9 @@ export function parseHashRoute(pathname: string): HashRoute {
|
|
|
27
28
|
if (path === "/tokens") {
|
|
28
29
|
return { view: "tokens" };
|
|
29
30
|
}
|
|
31
|
+
if (path === "/catalog" || path === "/governance/catalog") {
|
|
32
|
+
return { view: "catalog" };
|
|
33
|
+
}
|
|
30
34
|
if (path.startsWith("/component/")) {
|
|
31
35
|
const componentId = decodeURIComponent(path.slice("/component/".length));
|
|
32
36
|
if (componentId.length > 0) {
|
|
@@ -42,6 +46,8 @@ export function formatHashRoute(route: HashRoute): string {
|
|
|
42
46
|
return "/tokens";
|
|
43
47
|
case "governance":
|
|
44
48
|
return "/governance";
|
|
49
|
+
case "catalog":
|
|
50
|
+
return "/catalog";
|
|
45
51
|
case "component":
|
|
46
52
|
return `/component/${encodeURIComponent(route.componentId)}`;
|
|
47
53
|
default:
|
package/src/types/controls.ts
CHANGED
|
@@ -26,6 +26,16 @@ export type PlaygroundStringControl = {
|
|
|
26
26
|
placeholder?: string;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
export type PlaygroundNodeControl = {
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
type: "node";
|
|
33
|
+
default: string;
|
|
34
|
+
defaultSource?: PlaygroundDefaultSource;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
hint?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
29
39
|
export type PlaygroundNumberControl = {
|
|
30
40
|
key: string;
|
|
31
41
|
label: string;
|
|
@@ -49,6 +59,7 @@ export type PlaygroundSelectControl = {
|
|
|
49
59
|
export type PlaygroundControl =
|
|
50
60
|
| PlaygroundBooleanControl
|
|
51
61
|
| PlaygroundStringControl
|
|
62
|
+
| PlaygroundNodeControl
|
|
52
63
|
| PlaygroundNumberControl
|
|
53
64
|
| PlaygroundSelectControl;
|
|
54
65
|
|
|
@@ -61,6 +72,7 @@ export function defaultArgsFromControls(controls: PlaygroundControl[] | undefine
|
|
|
61
72
|
out[c.key] = c.default;
|
|
62
73
|
break;
|
|
63
74
|
case "string":
|
|
75
|
+
case "node":
|
|
64
76
|
case "select":
|
|
65
77
|
out[c.key] = c.default;
|
|
66
78
|
break;
|
package/src/types/report.ts
CHANGED
|
@@ -90,7 +90,7 @@ export interface UsageSummary {
|
|
|
90
90
|
* Simplified prop kind from TypeScript (e.g. demo `merge-playgrounds.mjs`).
|
|
91
91
|
* Dashboard falls back to name heuristics when a key is missing or kind is `unknown`.
|
|
92
92
|
*/
|
|
93
|
-
export type DeclaredPropKind = "boolean" | "string" | "number" | "unknown";
|
|
93
|
+
export type DeclaredPropKind = "boolean" | "string" | "number" | "node" | "unknown";
|
|
94
94
|
|
|
95
95
|
/** Emitted by dslint for dashboard playgrounds (no per-component TS registration). */
|
|
96
96
|
export interface PlaygroundSpec {
|
|
@@ -4,14 +4,16 @@ import { projectRootForConfig, readIncludeDirs } from "./collectScanModules";
|
|
|
4
4
|
|
|
5
5
|
const FALLBACK_INCLUDE_DIRS = ["resources/js", "src", "app"];
|
|
6
6
|
|
|
7
|
-
const DASHBOARD_SRC_MARKER = `${join("packages", "dashboard", "src")}`;
|
|
8
|
-
|
|
9
7
|
function normalizePosixPath(path: string): string {
|
|
10
8
|
return path.replace(/\\/g, "/").replace(/\/$/, "");
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
function isDashboardPackageSrc(absPath: string): boolean {
|
|
14
|
-
|
|
11
|
+
function isDashboardPackageSrc(absPath: string, packageRoot: string): boolean {
|
|
12
|
+
const dashboardSrc = normalizePosixPath(join(resolve(packageRoot), "src"));
|
|
13
|
+
const normalized = normalizePosixPath(absPath);
|
|
14
|
+
return (
|
|
15
|
+
normalized === dashboardSrc || normalized.startsWith(`${dashboardSrc}/`)
|
|
16
|
+
);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function resolveConsumerSourceAbsDirs(
|
|
@@ -34,7 +36,7 @@ function resolveConsumerSourceAbsDirs(
|
|
|
34
36
|
if (!norm) continue;
|
|
35
37
|
const abs = resolve(projectRoot, norm);
|
|
36
38
|
if (!existsSync(abs)) continue;
|
|
37
|
-
if (isDashboardPackageSrc(abs)) continue;
|
|
39
|
+
if (isDashboardPackageSrc(abs, packageRoot)) continue;
|
|
38
40
|
unique.add(abs);
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -42,7 +44,7 @@ function resolveConsumerSourceAbsDirs(
|
|
|
42
44
|
for (const dir of FALLBACK_INCLUDE_DIRS) {
|
|
43
45
|
const abs = resolve(scanAbs, dir);
|
|
44
46
|
if (!existsSync(abs)) continue;
|
|
45
|
-
if (isDashboardPackageSrc(abs)) continue;
|
|
47
|
+
if (isDashboardPackageSrc(abs, packageRoot)) continue;
|
|
46
48
|
unique.add(abs);
|
|
47
49
|
}
|
|
48
50
|
}
|