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.
Files changed (181) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +50 -29
  3. package/bin/dslinter.mjs +26 -5
  4. package/bin/lib/config-hide-component.mjs +44 -0
  5. package/bin/lib/config-hide-component.test.mjs +33 -0
  6. package/bin/lib/constants.mjs +20 -0
  7. package/bin/lib/dev-banner.mjs +16 -51
  8. package/bin/lib/dev-banner.test.mjs +20 -18
  9. package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
  10. package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
  11. package/bin/lib/enrich-report-cli.mjs +14 -0
  12. package/bin/lib/env.mjs +20 -0
  13. package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
  14. package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
  15. package/bin/lib/parse-args.mjs +13 -1
  16. package/bin/lib/parse-args.test.mjs +7 -1
  17. package/bin/lib/paths.mjs +8 -0
  18. package/bin/lib/project-root.mjs +72 -10
  19. package/bin/lib/project-root.test.mjs +32 -1
  20. package/bin/lib/prompt.mjs +31 -0
  21. package/bin/lib/resolve-project.mjs +78 -0
  22. package/bin/lib/resolve-project.test.mjs +74 -0
  23. package/bin/lib/run-scanner.mjs +40 -6
  24. package/bin/lib/scaffold-config.mjs +128 -9
  25. package/bin/lib/scaffold-config.test.mjs +24 -2
  26. package/bin/lib/scan-host.mjs +44 -0
  27. package/bin/lib/scan-host.test.mjs +41 -0
  28. package/bin/lib/setup-readiness.mjs +153 -0
  29. package/bin/lib/setup-readiness.test.mjs +32 -0
  30. package/bin/modes/build.mjs +31 -6
  31. package/bin/modes/dev.mjs +55 -21
  32. package/bin/modes/init.mjs +3 -22
  33. package/bin/modes/init.test.mjs +1 -1
  34. package/bin/modes/mcp.mjs +49 -0
  35. package/bin/modes/report.mjs +29 -4
  36. package/bin/modes/watch.mjs +85 -0
  37. package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
  38. package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
  39. package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
  40. package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
  41. package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
  42. package/dashboard-dist/index.html +2 -2
  43. package/index.cjs +53 -52
  44. package/index.d.ts +3 -0
  45. package/package.json +18 -12
  46. package/shared/env.ts +15 -0
  47. package/shared/paths.ts +8 -0
  48. package/shared/reportPath.test.ts +19 -0
  49. package/shared/reportPath.ts +12 -0
  50. package/shared/servePort.ts +16 -0
  51. package/src/components/ComponentInspectPane.tsx +67 -19
  52. package/src/components/ComponentPlaygroundPane.tsx +262 -113
  53. package/src/components/DashboardCommandPalette.tsx +6 -11
  54. package/src/components/GovernancePane.tsx +2 -2
  55. package/src/components/HideFromCatalogButton.tsx +44 -0
  56. package/src/components/OpenInEditorButton.tsx +36 -0
  57. package/src/components/PlaygroundA11yAndCode.tsx +53 -53
  58. package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
  59. package/src/components/PlaygroundControls.tsx +5 -11
  60. package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
  61. package/src/components/PlaygroundUsageCode.tsx +6 -4
  62. package/src/components/PlaygroundVariantMatrix.tsx +101 -34
  63. package/src/components/Section.tsx +5 -2
  64. package/src/components/Sidebar.tsx +131 -46
  65. package/src/components/TruncatedPath.tsx +44 -0
  66. package/src/components/controlApiTable.test.ts +29 -0
  67. package/src/components/controlApiTable.ts +3 -0
  68. package/src/components/playgroundUsageHighlight.ts +14 -3
  69. package/src/components/ui/badge.tsx +1 -1
  70. package/src/components/ui/table.tsx +2 -2
  71. package/src/dashboard/ComponentCatalog.tsx +16 -23
  72. package/src/dashboard/ComponentUsageDetails.tsx +6 -15
  73. package/src/dashboard/DashboardBody.tsx +0 -35
  74. package/src/dashboard/FindingsList.tsx +65 -55
  75. package/src/dashboard/ScannedTokenWall.tsx +3 -3
  76. package/src/dashboard/aggregate.test.ts +74 -0
  77. package/src/dashboard/aggregate.ts +145 -21
  78. package/src/dashboard/catalogVisibility.test.ts +93 -0
  79. package/src/dashboard/catalogVisibility.ts +108 -0
  80. package/src/dashboard/editorLink.test.ts +57 -0
  81. package/src/dashboard/editorLink.ts +71 -0
  82. package/src/dashboard/paths.test.ts +49 -0
  83. package/src/dashboard/paths.ts +51 -3
  84. package/src/dashboard/updateDslintConfig.ts +22 -0
  85. package/src/dashboard/useWorkspaceReport.ts +21 -17
  86. package/src/index.ts +26 -0
  87. package/src/mcp/agent-context.ts +148 -0
  88. package/src/mcp/agent-query.test.ts +89 -0
  89. package/src/mcp/agent-query.ts +373 -0
  90. package/src/mcp/config.ts +53 -0
  91. package/src/mcp/index.ts +18 -0
  92. package/src/mcp/normalize-paths.ts +65 -0
  93. package/src/mcp/report-cache.ts +212 -0
  94. package/src/mcp/rule-catalog.json +156 -0
  95. package/src/mcp/rule-catalog.ts +33 -0
  96. package/src/mcp/schemas.ts +54 -0
  97. package/src/mcp/server.test.ts +44 -0
  98. package/src/mcp/server.ts +343 -0
  99. package/src/mcp/start.ts +29 -0
  100. package/src/mcp/verify-loop.test.ts +49 -0
  101. package/src/mcp/verify-loop.ts +149 -0
  102. package/src/playground/appPreviewTheme.test.ts +148 -0
  103. package/src/playground/appPreviewTheme.ts +137 -0
  104. package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
  105. package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
  106. package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
  107. package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
  108. package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
  109. package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
  110. package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
  111. package/src/playground/collectDefinedPlaygrounds.ts +68 -0
  112. package/src/playground/controls.ts +177 -0
  113. package/src/playground/createPlaygroundRegistry.ts +1 -1
  114. package/src/playground/definePlayground.tsx +88 -16
  115. package/src/playground/definePlaygroundFromKit.ts +17 -0
  116. package/src/playground/embedGlobKey.ts +8 -0
  117. package/src/playground/enrichKitControls.test.ts +25 -0
  118. package/src/playground/enrichKitControls.ts +197 -0
  119. package/src/playground/expandPlaygroundControls.test.ts +50 -0
  120. package/src/playground/expandPlaygroundControls.ts +97 -0
  121. package/src/playground/inferKitJsx.test.ts +77 -0
  122. package/src/playground/inferKitJsx.ts +165 -0
  123. package/src/playground/inferKitParams.test.ts +41 -0
  124. package/src/playground/inferKitParams.ts +113 -0
  125. package/src/playground/inferPropTypesFromTs.d.mts +47 -0
  126. package/src/playground/inferPropTypesFromTs.mjs +343 -0
  127. package/src/playground/inferPropTypesFromTs.test.ts +227 -0
  128. package/src/playground/inferPropTypesFromTs.ts +17 -0
  129. package/src/playground/mergePlaygroundEntries.test.ts +32 -0
  130. package/src/playground/mergePlaygroundEntries.ts +28 -0
  131. package/src/playground/playgroundJoin.test.ts +79 -19
  132. package/src/playground/playgroundJoin.ts +47 -22
  133. package/src/playground/playgroundModuleExport.test.ts +42 -0
  134. package/src/playground/playgroundModuleExport.ts +22 -0
  135. package/src/playground/playgroundSpecsKey.ts +8 -0
  136. package/src/playground/propCoerce.ts +91 -0
  137. package/src/playground/scanVariantA11y.test.ts +46 -0
  138. package/src/playground/scanVariantA11y.ts +107 -0
  139. package/src/playground/snippet.ts +83 -0
  140. package/src/playground/usePlaygroundFromReport.test.ts +18 -8
  141. package/src/playground/usePlaygroundFromReport.ts +3 -1
  142. package/src/report/a11yForModule.ts +2 -7
  143. package/src/report/a11yScoring.test.ts +24 -0
  144. package/src/report/a11yScoring.ts +17 -0
  145. package/src/report/index.ts +6 -0
  146. package/src/shell/DashboardLayout.tsx +71 -45
  147. package/src/shell/DashboardLayoutAuto.tsx +0 -4
  148. package/src/shell/hashRoute.test.ts +7 -15
  149. package/src/shell/hashRoute.ts +31 -31
  150. package/src/shell/useHashRoute.ts +38 -13
  151. package/src/styles/dashboard-theme.css +18 -7
  152. package/src/types/controls.ts +11 -0
  153. package/src/types/playground.ts +4 -0
  154. package/src/types/report.ts +32 -9
  155. package/templates/playground/buildRegistry.ts +1 -1
  156. package/templates/vite.dslinter.snippet.ts +15 -4
  157. package/vite/collectScanModules.test.ts +91 -3
  158. package/vite/collectScanModules.ts +94 -29
  159. package/vite/consumer.config.mjs +6 -3
  160. package/vite/consumerAlias.test.ts +47 -0
  161. package/vite/consumerAlias.ts +114 -0
  162. package/vite/embedTailwindSources.test.ts +74 -0
  163. package/vite/embedTailwindSources.ts +97 -0
  164. package/vite/loadConsumerAliases.test.ts +131 -0
  165. package/vite/loadConsumerAliases.ts +155 -0
  166. package/vite/openFileInEditor.mjs +196 -0
  167. package/vite/openFileInEditor.test.mjs +87 -0
  168. package/vite/plugin.resolve.test.ts +72 -0
  169. package/vite/plugin.ts +216 -19
  170. package/vite/reportPath.test.ts +19 -0
  171. package/vite/resolveWayfinderImport.ts +56 -0
  172. package/vite/shims/inertia-react.tsx +85 -0
  173. package/vite/shims/wayfinder-actions.ts +33 -0
  174. package/vite/shims/wayfinder-routes.ts +30 -0
  175. package/vite/shims/ziggy-js.ts +12 -0
  176. package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
  177. package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
  178. package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
  179. package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
  180. package/src/components/playgroundUsageTwoslash.ts +0 -69
  181. package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
@@ -1,14 +1,23 @@
1
+ import { createElement, type ReactNode } from "react";
2
+ import { renderToStaticMarkup } from "react-dom/server";
1
3
  import { describe, expect, it } from "vitest";
2
4
  import type { PlaygroundEntry } from "../types/playground";
3
5
  import type { WorkspaceReport } from "../types/report";
4
- import { resolvePlaygroundEntry } from "./buildPlaygroundEntriesFromReport";
6
+ import { defaultArgsFromControls, type PlaygroundControl } from "../types/controls";
7
+ import {
8
+ buildPlaygroundEntriesFromReportWithSkips,
9
+ resolvePlaygroundEntry,
10
+ } from "./buildPlaygroundEntriesFromReport";
11
+ import { definePlayground } from "./definePlayground";
12
+ import { inferKitRootPropBindings } from "./inferKitJsx";
5
13
 
6
14
  const entries: PlaygroundEntry[] = [
7
15
  {
8
16
  id: "PrimaryButton",
9
17
  meta: { id: "PrimaryButton", title: "PrimaryButton" },
10
- modulePath: "@dslint-scan/src/components/PrimaryButton.tsx",
18
+ modulePath: "@dslinter-scan/src/components/PrimaryButton.tsx",
11
19
  controls: [],
20
+ renderPreview: () => null,
12
21
  Preview: () => null,
13
22
  },
14
23
  ];
@@ -30,12 +39,11 @@ describe("playground catalog id alignment", () => {
30
39
  files: [],
31
40
  findings: [],
32
41
  scores: {
33
- system_health: 0,
34
- token_adoption: 0,
35
- component_adoption: 0,
42
+ design_system_health: 0,
36
43
  ux_consistency: 0,
44
+ accessibility: 0,
45
+ maintainability: 0,
37
46
  },
38
- ownership: [],
39
47
  duplicate_components: [],
40
48
  usage_by_component: [],
41
49
  playgrounds: [
@@ -51,3 +59,409 @@ describe("playground catalog id alignment", () => {
51
59
  expect(report.playgrounds?.[0]?.export_name).toBe("Card");
52
60
  });
53
61
  });
62
+
63
+ describe("playground preview props", () => {
64
+ const report: WorkspaceReport = {
65
+ root: "/repo",
66
+ files: [],
67
+ findings: [],
68
+ scores: {
69
+ design_system_health: 0,
70
+ ux_consistency: 0,
71
+ accessibility: 0,
72
+ maintainability: 0,
73
+ },
74
+ duplicate_components: [],
75
+ usage_by_component: [],
76
+ playgrounds: [
77
+ {
78
+ id: "Demo",
79
+ export_name: "Demo",
80
+ rel_path: "src/components/Demo.tsx",
81
+ declared_props: ["children"],
82
+ },
83
+ ],
84
+ };
85
+
86
+ it("maps playground control values even when declared_props omits them", () => {
87
+ const controlOverrides: Record<string, PlaygroundControl[]> = {
88
+ Demo: [
89
+ { key: "children", label: "children", type: "string", default: "" },
90
+ {
91
+ key: "variant",
92
+ label: "variant",
93
+ type: "select",
94
+ default: "primary",
95
+ options: [
96
+ { value: "primary", label: "primary" },
97
+ { value: "muted", label: "muted" },
98
+ ],
99
+ },
100
+ ],
101
+ };
102
+ let lastProps: Record<string, unknown> | null = null;
103
+ const modules = {
104
+ "../components/Demo.tsx": {
105
+ Demo: (props: Record<string, unknown>) => {
106
+ lastProps = props;
107
+ return createElement("span", null, String(props.children ?? ""));
108
+ },
109
+ },
110
+ };
111
+ const { entries } = buildPlaygroundEntriesFromReportWithSkips(report, modules, {
112
+ globKeyFromRelPath: (rel) => `../components/${rel.split("/").pop()}`,
113
+ controlOverrides,
114
+ logJoinSkips: false,
115
+ });
116
+ const entry = entries[0];
117
+ expect(entry).toBeDefined();
118
+ renderToStaticMarkup(
119
+ createElement(entry!.Preview, {
120
+ values: { children: "Hello", variant: "muted" },
121
+ }),
122
+ );
123
+ expect(lastProps).toMatchObject({ children: "Hello", variant: "muted" });
124
+ });
125
+
126
+ it("builds select controls from declared_prop_options (CVA)", () => {
127
+ const cvaReport: WorkspaceReport = {
128
+ ...report,
129
+ playgrounds: [
130
+ {
131
+ id: "Button",
132
+ export_name: "Button",
133
+ rel_path: "src/components/ui/button.tsx",
134
+ declared_props: ["variant", "size", "asChild"],
135
+ declared_prop_options: {
136
+ variant: ["default", "destructive", "outline"],
137
+ size: ["default", "sm", "lg"],
138
+ },
139
+ declared_prop_defaults: {
140
+ variant: "default",
141
+ size: "default",
142
+ },
143
+ },
144
+ ],
145
+ };
146
+ let lastProps: Record<string, unknown> | null = null;
147
+ const modules = {
148
+ "../components/ui/button.tsx": {
149
+ Button: (props: Record<string, unknown>) => {
150
+ lastProps = props;
151
+ return createElement("button", props, props.children as ReactNode);
152
+ },
153
+ },
154
+ };
155
+ const { entries } = buildPlaygroundEntriesFromReportWithSkips(cvaReport, modules, {
156
+ globKeyFromRelPath: (rel) => `../components/ui/${rel.split("/").pop()}`,
157
+ logJoinSkips: false,
158
+ });
159
+ const entry = entries[0];
160
+ expect(entry).toBeDefined();
161
+ const variant = entry!.controls.find((c) => c.key === "variant");
162
+ expect(variant?.type).toBe("select");
163
+ if (variant?.type === "select") {
164
+ expect(variant.default).toBe("default");
165
+ expect(variant.options.map((o) => o.value)).toEqual(["default", "destructive", "outline"]);
166
+ }
167
+ const asChild = entry!.controls.find((c) => c.key === "asChild");
168
+ expect(asChild?.type).toBe("boolean");
169
+
170
+ const children = entry!.controls.find((c) => c.key === "children");
171
+ expect(children?.type).toBe("string");
172
+ if (children?.type === "string") {
173
+ expect(children.default).toBe("Example");
174
+ }
175
+
176
+ renderToStaticMarkup(createElement(entry!.Preview, { values: {} }));
177
+ expect(lastProps).toMatchObject({ children: "Example" });
178
+ const defaultValues = defaultArgsFromControls(entry!.controls);
179
+ expect(entry!.usageSnippet?.(defaultValues)).toBe("<Button>Example</Button>");
180
+
181
+ renderToStaticMarkup(createElement(entry!.Preview, { values: { children: "" } }));
182
+ expect(lastProps).toMatchObject({ children: "" });
183
+ expect(entry!.usageSnippet?.({ ...defaultValues, children: "" })).toBe("<Button />");
184
+ });
185
+
186
+ it("adds children control from repo usage when declared_props omits it", () => {
187
+ const badgeReport: WorkspaceReport = {
188
+ ...report,
189
+ usage_by_component: [
190
+ {
191
+ component: "Badge",
192
+ reference_count: 3,
193
+ file_count: 2,
194
+ max_props_on_single_use: 2,
195
+ files: [],
196
+ prop_frequencies: { children: 3 },
197
+ },
198
+ ],
199
+ playgrounds: [
200
+ {
201
+ id: "Badge",
202
+ export_name: "Badge",
203
+ rel_path: "src/components/ui/badge.tsx",
204
+ declared_props: ["variant"],
205
+ declared_prop_options: {
206
+ variant: ["default", "secondary"],
207
+ },
208
+ declared_prop_defaults: { variant: "default" },
209
+ },
210
+ ],
211
+ };
212
+ let lastProps: Record<string, unknown> | null = null;
213
+ const modules = {
214
+ "../components/ui/badge.tsx": {
215
+ Badge: (props: Record<string, unknown>) => {
216
+ lastProps = props;
217
+ return createElement("span", props, props.children as ReactNode);
218
+ },
219
+ },
220
+ };
221
+ const { entries } = buildPlaygroundEntriesFromReportWithSkips(badgeReport, modules, {
222
+ globKeyFromRelPath: (rel) => `../components/ui/${rel.split("/").pop()}`,
223
+ logJoinSkips: false,
224
+ });
225
+ const entry = entries[0];
226
+ expect(entry).toBeDefined();
227
+ expect(entry!.controls.some((c) => c.key === "children")).toBe(true);
228
+ renderToStaticMarkup(createElement(entry!.Preview, { values: {} }));
229
+ expect(lastProps).toMatchObject({ children: "Example" });
230
+ const defaultValues = defaultArgsFromControls(entry!.controls);
231
+ expect(entry!.usageSnippet?.(defaultValues)).toBe("<Badge>Example</Badge>");
232
+ });
233
+
234
+ it("manual kit playground merges CVA variant select from report", () => {
235
+ function Alert(props: { variant?: string; children?: ReactNode }) {
236
+ return createElement("div", { "data-slot": "alert", "data-variant": props.variant }, props.children);
237
+ }
238
+ function AlertTitle(props: { children?: ReactNode }) {
239
+ return createElement("div", { "data-slot": "alert-title" }, props.children);
240
+ }
241
+ function AlertDescription(props: { children?: ReactNode }) {
242
+ return createElement("div", { "data-slot": "alert-description" }, props.children);
243
+ }
244
+
245
+ const kit = ({ title, description, variant }: { title: string; description: string; variant: string }) =>
246
+ createElement(
247
+ Alert,
248
+ { variant },
249
+ createElement(AlertTitle, null, title),
250
+ createElement(AlertDescription, null, description),
251
+ );
252
+ expect(inferKitRootPropBindings(kit)).toEqual([
253
+ { component: "Alert", prop: "variant", param: expect.stringMatching(/^variant\d*$/) },
254
+ ]);
255
+ const defined = definePlayground(kit);
256
+
257
+ const alertReport: WorkspaceReport = {
258
+ ...report,
259
+ playgrounds: [
260
+ {
261
+ id: "Alert",
262
+ export_name: "Alert",
263
+ rel_path: "resources/js/components/ui/alert.tsx",
264
+ declared_props: ["variant"],
265
+ declared_prop_options: {
266
+ variant: ["default", "destructive"],
267
+ },
268
+ declared_prop_defaults: {
269
+ variant: "default",
270
+ },
271
+ },
272
+ ],
273
+ };
274
+ const modules = {
275
+ "../components/ui/alert.tsx": {
276
+ Alert: () => createElement("div", null, "auto alert"),
277
+ },
278
+ "../components/ui/alert.playground.tsx": {
279
+ alertPlayground: defined,
280
+ },
281
+ };
282
+ const { entries } = buildPlaygroundEntriesFromReportWithSkips(alertReport, modules, {
283
+ globKeyFromRelPath: (rel) => {
284
+ const name = rel.split("/").pop()!;
285
+ return `../components/ui/${name}`;
286
+ },
287
+ logJoinSkips: false,
288
+ });
289
+ const entry = entries.find((e) => e.id === "Alert");
290
+ expect(entry).toBeDefined();
291
+ const variant = entry!.controls.find((c) => c.key === "variant");
292
+ expect(variant?.type).toBe("select");
293
+ if (variant?.type === "select") {
294
+ expect(variant.options.map((o) => o.value)).toEqual(["default", "destructive"]);
295
+ }
296
+ const title = entry!.controls.find((c) => c.key === "title");
297
+ expect(title?.label).toBe("Title");
298
+ });
299
+
300
+ it("definePlayground kit manual entries override auto previews with inferred id", () => {
301
+ const alertReport: WorkspaceReport = {
302
+ ...report,
303
+ playgrounds: [
304
+ {
305
+ id: "Alert",
306
+ export_name: "Alert",
307
+ rel_path: "resources/js/components/ui/alert.tsx",
308
+ declared_props: ["variant"],
309
+ },
310
+ ],
311
+ };
312
+ const defined = definePlayground(({ title = "Heads up" }) =>
313
+ createElement("div", { "data-slot": "alert" }, String(title)),
314
+ );
315
+ const modules = {
316
+ "../components/ui/alert.tsx": {
317
+ Alert: () => createElement("div", null, "auto alert"),
318
+ },
319
+ "../components/ui/alert.playground.tsx": {
320
+ alertPlayground: defined,
321
+ },
322
+ };
323
+ const { entries } = buildPlaygroundEntriesFromReportWithSkips(alertReport, modules, {
324
+ globKeyFromRelPath: (rel) => {
325
+ const name = rel.split("/").pop()!;
326
+ return `../components/ui/${name}`;
327
+ },
328
+ logJoinSkips: false,
329
+ });
330
+ expect(entries).toHaveLength(1);
331
+ expect(entries[0]?.id).toBe("Alert");
332
+ expect(
333
+ renderToStaticMarkup(
334
+ createElement(entries[0]!.Preview, { values: defaultArgsFromControls(entries[0]!.controls) }),
335
+ ),
336
+ ).toBe('<div data-slot="alert">Heads up</div>');
337
+ });
338
+
339
+ it("upgrades compound root entry to composed preview with inferred controls", () => {
340
+ const relPath = "resources/js/components/ui/dropdown-menu.tsx";
341
+ const dropdownReport: WorkspaceReport = {
342
+ ...report,
343
+ files: [
344
+ {
345
+ path: `/repo/${relPath}`,
346
+ definitions: [
347
+ { name: "DropdownMenu", kind: "function", line: 1, declared_props: ["open"] },
348
+ {
349
+ name: "DropdownMenuTrigger",
350
+ kind: "function",
351
+ line: 2,
352
+ declared_props: ["children"],
353
+ },
354
+ {
355
+ name: "DropdownMenuContent",
356
+ kind: "function",
357
+ line: 3,
358
+ declared_props: ["align", "sideOffset"],
359
+ },
360
+ {
361
+ name: "DropdownMenuItem",
362
+ kind: "function",
363
+ line: 4,
364
+ declared_props: ["variant"],
365
+ },
366
+ ],
367
+ usages: [],
368
+ parse_errors: [],
369
+ },
370
+ ],
371
+ usage_by_component: [
372
+ {
373
+ component: "DropdownMenuContent",
374
+ reference_count: 10,
375
+ file_count: 5,
376
+ max_props_on_single_use: 2,
377
+ files: [],
378
+ prop_frequencies: { align: 10 },
379
+ prop_value_frequencies: { align: { end: 8, start: 2 } },
380
+ },
381
+ ],
382
+ playgrounds: [
383
+ {
384
+ id: "DropdownMenu",
385
+ export_name: "DropdownMenu",
386
+ rel_path: relPath,
387
+ declared_props: ["open"],
388
+ },
389
+ ],
390
+ };
391
+ const modules = {
392
+ "../components/ui/dropdown-menu.tsx": {
393
+ DropdownMenu: ({ children }: { children?: ReactNode }) =>
394
+ createElement("div", { "data-root": "dropdown-menu" }, children),
395
+ DropdownMenuTrigger: ({ children }: { children?: ReactNode }) =>
396
+ createElement("button", { type: "button" }, children),
397
+ DropdownMenuContent: ({
398
+ children,
399
+ align,
400
+ }: {
401
+ children?: ReactNode;
402
+ align?: string;
403
+ }) => createElement("div", { "data-content": true, "data-align": align }, children),
404
+ DropdownMenuItem: ({ children }: { children?: ReactNode }) =>
405
+ createElement("div", { "data-item": true }, children),
406
+ },
407
+ };
408
+ const { entries } = buildPlaygroundEntriesFromReportWithSkips(dropdownReport, modules, {
409
+ globKeyFromRelPath: (rel) => {
410
+ const name = rel.split("/").pop()!;
411
+ return `../components/ui/${name}`;
412
+ },
413
+ logJoinSkips: false,
414
+ });
415
+ const root = resolvePlaygroundEntry(entries, "DropdownMenu");
416
+ expect(root).toBeDefined();
417
+ expect(root!.id).toBe("DropdownMenu");
418
+ const align = root!.controls.find((c) => c.key === "align");
419
+ expect(align?.type).toBe("select");
420
+ if (align?.type === "select") {
421
+ expect(align.options.map((o) => o.value)).toEqual(["end", "start"]);
422
+ }
423
+ const html = renderToStaticMarkup(createElement(root!.Preview, { values: { align: "end" } }));
424
+ expect(html).toContain('data-root="dropdown-menu"');
425
+ expect(html).toContain("Open menu");
426
+ expect(html).toContain('data-content="true"');
427
+ expect(html).toContain('data-align="end"');
428
+ });
429
+
430
+ it("manual definePlayground entries override auto-generated previews", () => {
431
+ const dropdownReport: WorkspaceReport = {
432
+ ...report,
433
+ playgrounds: [
434
+ {
435
+ id: "DropdownMenu",
436
+ export_name: "DropdownMenu",
437
+ rel_path: "resources/js/components/ui/dropdown-menu.tsx",
438
+ declared_props: [],
439
+ },
440
+ ],
441
+ };
442
+ const defined = definePlayground(
443
+ () => createElement("nav", null, "manual menu"),
444
+ { id: "DropdownMenu", group: "ui" },
445
+ );
446
+ const modules = {
447
+ "../components/ui/dropdown-menu.tsx": {
448
+ DropdownMenu: () => createElement("div", null, "auto menu"),
449
+ },
450
+ "../components/ui/dropdown-menu.playground.tsx": {
451
+ dropdownMenuPlayground: defined,
452
+ },
453
+ };
454
+ const { entries } = buildPlaygroundEntriesFromReportWithSkips(dropdownReport, modules, {
455
+ globKeyFromRelPath: (rel) => {
456
+ const name = rel.split("/").pop()!;
457
+ return `../components/ui/${name}`;
458
+ },
459
+ logJoinSkips: false,
460
+ });
461
+ expect(entries).toHaveLength(1);
462
+ expect(entries[0]?.meta.group).toBe("ui");
463
+ expect(renderToStaticMarkup(createElement(entries[0]!.Preview, { values: {} }))).toBe(
464
+ "<nav>manual menu</nav>",
465
+ );
466
+ });
467
+ });