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.
Files changed (59) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/bin/lib/infer-prop-types-from-ts.mjs +14 -1
  3. package/bin/lib/infer-prop-types-from-ts.test.mjs +32 -0
  4. package/dashboard-dist/assets/{DashboardLayoutAuto-h0gP_iKd.js → DashboardLayoutAuto-B4P-sy4z.js} +1 -1
  5. package/dashboard-dist/assets/DashboardLayoutAuto-CAO6F6-q.css +1 -0
  6. package/dashboard-dist/assets/{axe-DDaE9JTN.js → axe-CaxTXfM9.js} +1 -1
  7. package/dashboard-dist/assets/index-B432JkIx.js +219 -0
  8. package/dashboard-dist/assets/index-D0O_5w5V.css +1 -0
  9. package/dashboard-dist/dslinter-report.json +23929 -0
  10. package/dashboard-dist/index.html +2 -2
  11. package/index.cjs +52 -52
  12. package/package.json +6 -6
  13. package/src/components/CatalogPane.tsx +94 -0
  14. package/src/components/ComponentInspectPane.tsx +125 -125
  15. package/src/components/ComponentPlaygroundPane.tsx +42 -27
  16. package/src/components/DashboardCommandPalette.tsx +9 -0
  17. package/src/components/GovernanceInventoryTabs.tsx +51 -0
  18. package/src/components/GovernancePane.tsx +18 -5
  19. package/src/components/PlaygroundA11yAndCode.tsx +0 -52
  20. package/src/components/PlaygroundControlField.tsx +2 -0
  21. package/src/components/ScoreGauge.test.ts +22 -0
  22. package/src/components/ScoreGauge.tsx +179 -0
  23. package/src/components/Sidebar.tsx +97 -23
  24. package/src/components/TokensPane.tsx +11 -13
  25. package/src/components/controlApiTable.test.ts +15 -0
  26. package/src/components/controlApiTable.ts +4 -0
  27. package/src/components/ui/badge.tsx +5 -5
  28. package/src/dashboard/ComponentCatalog.tsx +10 -1
  29. package/src/dashboard/ComponentPropUsageDetail.tsx +127 -42
  30. package/src/dashboard/ComponentUsageDetails.tsx +39 -9
  31. package/src/dashboard/DashboardBody.tsx +83 -12
  32. package/src/dashboard/ScannedTokenWall.tsx +9 -6
  33. package/src/dashboard/UnusedComponentsList.tsx +74 -0
  34. package/src/dashboard/aggregate.test.ts +381 -12
  35. package/src/dashboard/aggregate.ts +167 -30
  36. package/src/dashboard/mergeTokenCatalog.ts +5 -0
  37. package/src/dashboard/paths.test.ts +18 -1
  38. package/src/dashboard/paths.ts +8 -0
  39. package/src/mcp/agent-query.ts +1 -1
  40. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +3 -3
  41. package/src/playground/controls.ts +16 -3
  42. package/src/playground/enrichKitControls.ts +5 -5
  43. package/src/playground/inferKitJsx.test.ts +0 -11
  44. package/src/playground/inferPropTypesFromTs.d.mts +1 -1
  45. package/src/playground/inferPropTypesFromTs.mjs +19 -3
  46. package/src/playground/inferPropTypesFromTs.test.ts +32 -0
  47. package/src/playground/inferPropTypesFromTs.ts +1 -1
  48. package/src/playground/playgroundJoin.ts +34 -0
  49. package/src/playground/propCoerce.ts +2 -2
  50. package/src/playground/snippet.ts +1 -0
  51. package/src/shell/DashboardLayout.tsx +21 -4
  52. package/src/shell/hashRoute.test.ts +9 -0
  53. package/src/shell/hashRoute.ts +6 -0
  54. package/src/types/controls.ts +12 -0
  55. package/src/types/report.ts +1 -1
  56. package/vite/embedTailwindSources.ts +8 -6
  57. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +0 -1
  58. package/dashboard-dist/assets/index-B9sZ6wHm.css +0 -1
  59. 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);
@@ -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.
@@ -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, ruleById, ruleCatalog } from "./rule-catalog";
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: "string", default: "" },
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("string");
172
- if (children?.type === "string") {
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): PlaygroundStringControl {
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: "string",
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: "string",
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
- ...(def?.declared_prop_options ?? {}),
106
- ...(spec.declared_prop_options ?? {}),
105
+ ...def?.declared_prop_options,
106
+ ...spec.declared_prop_options,
107
107
  };
108
108
  const propDefaults = {
109
- ...(def?.declared_prop_defaults ?? {}),
110
- ...(spec.declared_prop_defaults ?? {}),
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
- return followTypeAlias(checker, checker.getDeclaredTypeOfSymbol(type.aliasSymbol));
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
  }
@@ -17,6 +17,7 @@ function valueMatchesPlaygroundDefault(
17
17
  return Number.isFinite(n) && n === control.default;
18
18
  }
19
19
  case "string":
20
+ case "node":
20
21
  case "select":
21
22
  return String(value ?? "") === String(control.default);
22
23
  default:
@@ -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
- { name: item.parent, label: item.parent },
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
  });
@@ -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:
@@ -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;
@@ -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
- return normalizePosixPath(absPath).endsWith(DASHBOARD_SRC_MARKER);
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
  }