dslinter 0.1.5 → 0.2.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 (181) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +54 -27
  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 +92 -24
  19. package/bin/lib/project-root.test.mjs +52 -0
  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 +163 -0
  25. package/bin/lib/scaffold-config.test.mjs +43 -0
  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 +56 -13
  32. package/bin/modes/init.mjs +35 -47
  33. package/bin/modes/init.test.mjs +16 -0
  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 +209 -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 +51 -3
  158. package/vite/collectScanModules.ts +85 -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-BPPtPsYh.css +0 -1
  177. package/dashboard-dist/assets/DashboardLayoutAuto-Dp3bAQxH.js +0 -1
  178. package/dashboard-dist/assets/index-DsjwnDdX.js +0 -206
  179. package/dashboard-dist/assets/index-jaCmZJlW.css +0 -1
  180. package/src/components/playgroundUsageTwoslash.ts +0 -69
  181. package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
@@ -1,13 +1,108 @@
1
- import { createElement, type ComponentType } from "react";
1
+ import { createElement } from "react";
2
2
  import type { PlaygroundArgs, PlaygroundControl } from "../types/controls";
3
- import type { DeclaredPropKind, PlaygroundSpec, WorkspaceReport } from "../types/report";
4
- import type { PlaygroundEntry, PlaygroundMeta, PlaygroundPreviewComponent } from "../types/playground";
3
+ import type { PlaygroundSpec, UsageSummary, WorkspaceReport } from "../types/report";
4
+ import type { PlaygroundEntry, PlaygroundMeta } from "../types/playground";
5
+ import type { PlaygroundPreviewComponent } from "../types/preview";
5
6
  import {
6
7
  defaultEmbedGlobKeyFromRelPath,
7
8
  diagnosePlaygroundJoinSkips,
8
9
  logPlaygroundJoinSkips,
10
+ resolveModuleKeyForRelPath,
9
11
  type PlaygroundJoinSkip,
10
12
  } from "./playgroundJoin";
13
+ import {
14
+ definitionPathsForName,
15
+ isCatalogComponentHidden,
16
+ } from "../dashboard/catalogVisibility";
17
+ import { collectDefinedPlaygrounds } from "./collectDefinedPlaygrounds";
18
+ import { catalogIdFromPlaygroundExport } from "./catalogIdFromPlaygroundExport";
19
+ import {
20
+ buildCompoundPlaygroundEntries,
21
+ upgradeRootEntriesWithCompoundPreview,
22
+ } from "./buildCompoundPlaygroundEntries";
23
+ import { mergePlaygroundEntries } from "./mergePlaygroundEntries";
24
+ import { getModuleExport } from "./playgroundModuleExport";
25
+ import { mergeReportControlsForKit, type PlaygroundKitHints } from "./enrichKitControls";
26
+ import {
27
+ componentAcceptsChildren,
28
+ controlsForSpec,
29
+ ensureChildrenControl,
30
+ } from "./controls";
31
+ import { genericUsageSnippet } from "./snippet";
32
+ import { mergeStaticDefaults, normalizedPropKinds, valuesToComponentProps } from "./propCoerce";
33
+
34
+ function isDefinedPlayground(value: unknown): value is import("./definePlayground").DefinedPlayground {
35
+ if (!value || typeof value !== "object") return false;
36
+ const o = value as Record<string, unknown>;
37
+ if (typeof o.playgroundMeta !== "object" || o.playgroundMeta === null) return false;
38
+ const meta = o.playgroundMeta as { id?: unknown; title?: unknown; group?: unknown };
39
+ return (
40
+ typeof meta.id === "string" &&
41
+ typeof meta.title === "string" &&
42
+ (meta.group === undefined || typeof meta.group === "string") &&
43
+ Array.isArray(o.playgroundControls) &&
44
+ typeof o.PlaygroundPreview === "function"
45
+ );
46
+ }
47
+
48
+ function specForCatalogEntry(
49
+ report: WorkspaceReport,
50
+ catalogId: string,
51
+ ): PlaygroundSpec | undefined {
52
+ return report.playgrounds?.find(
53
+ (spec) => spec.export_name === catalogId || spec.id === catalogId,
54
+ );
55
+ }
56
+
57
+ /** When JSX inference fails, still merge CVA props whose keys match kit control params. */
58
+ function fallbackRootPropBindings(
59
+ entry: PlaygroundEntry,
60
+ hints: PlaygroundKitHints | undefined,
61
+ report: WorkspaceReport,
62
+ ): PlaygroundKitHints["rootPropBindings"] {
63
+ const fromKit = hints?.rootPropBindings ?? [];
64
+ if (fromKit.length > 0) return fromKit;
65
+
66
+ const spec = specForCatalogEntry(report, entry.id);
67
+ if (!spec) return [];
68
+
69
+ const paramKeys = new Set(entry.controls.map((control) => control.key));
70
+ return (spec.declared_props ?? [])
71
+ .filter((prop) => paramKeys.has(prop))
72
+ .map((prop) => ({ component: spec.export_name, prop, param: prop }));
73
+ }
74
+
75
+ function enrichManualEntriesFromReport(
76
+ entries: PlaygroundEntry[],
77
+ modules: BuildPlaygroundModules,
78
+ report: WorkspaceReport | null | undefined,
79
+ ): PlaygroundEntry[] {
80
+ if (!report) return entries;
81
+
82
+ const definedById = new Map<string, import("./definePlayground").DefinedPlayground>();
83
+ for (const mod of Object.values(modules)) {
84
+ if (!mod || typeof mod !== "object") continue;
85
+ for (const [exportName, value] of Object.entries(mod)) {
86
+ if (!isDefinedPlayground(value)) continue;
87
+ const catalogId =
88
+ value.playgroundMeta.id || catalogIdFromPlaygroundExport(exportName) || "";
89
+ if (!catalogId) continue;
90
+ definedById.set(catalogId, value);
91
+ }
92
+ }
93
+
94
+ return entries.map((entry) => {
95
+ const defined = definedById.get(entry.id);
96
+ const bindings = fallbackRootPropBindings(
97
+ entry,
98
+ defined?.playgroundKitHints,
99
+ report,
100
+ );
101
+ if (!bindings.length) return entry;
102
+ const controls = mergeReportControlsForKit(entry.controls, bindings, report, entry.id);
103
+ return controls === entry.controls ? entry : { ...entry, controls };
104
+ });
105
+ }
11
106
 
12
107
  export type BuildPlaygroundModules = Record<string, Record<string, unknown>>;
13
108
 
@@ -16,7 +111,7 @@ export type BuildPlaygroundOptions = {
16
111
  globKeyFromRelPath?: (relPath: string) => string;
17
112
  controlOverrides?: Record<string, PlaygroundControl[]>;
18
113
  staticDefaults?: Record<string, Record<string, unknown>>;
19
- /** When true (default in Vite dev), log specs that failed to join to `modules`. */
114
+ /** When true, log specs that failed to join to `modules` (inspect pane still shows skips). */
20
115
  logJoinSkips?: boolean;
21
116
  };
22
117
 
@@ -25,8 +120,11 @@ export type BuildPlaygroundResult = {
25
120
  skipped: PlaygroundJoinSkip[];
26
121
  };
27
122
 
28
- function defaultGlobKeyFromRelPath(relPath: string): string {
29
- return defaultEmbedGlobKeyFromRelPath(relPath);
123
+ function usageForExportName(
124
+ report: WorkspaceReport | null | undefined,
125
+ exportName: string,
126
+ ): UsageSummary | undefined {
127
+ return report?.usage_by_component?.find((u) => u.component === exportName);
30
128
  }
31
129
 
32
130
  export type { PlaygroundJoinSkip, PlaygroundJoinSkipReason } from "./playgroundJoin";
@@ -40,248 +138,6 @@ export {
40
138
  logPlaygroundJoinSkips,
41
139
  } from "./playgroundJoin";
42
140
 
43
- function coerceDeclaredPropKind(v: unknown): DeclaredPropKind | undefined {
44
- if (v === "boolean" || v === "string" || v === "number" || v === "unknown")
45
- return v;
46
- return undefined;
47
- }
48
-
49
- function normalizedPropKinds(
50
- raw: PlaygroundSpec["declared_prop_kinds"],
51
- ): Partial<Record<string, DeclaredPropKind>> | undefined {
52
- if (!raw || typeof raw !== "object") return undefined;
53
- const out: Partial<Record<string, DeclaredPropKind>> = {};
54
- for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
55
- const ck = coerceDeclaredPropKind(v);
56
- if (ck && ck !== "unknown") out[k] = ck;
57
- }
58
- return Object.keys(out).length ? out : undefined;
59
- }
60
-
61
- function isLikelyBooleanProp(name: string): boolean {
62
- const n = name.toLowerCase();
63
- if (n === "disabled" || n === "loading") return true;
64
- if (n.startsWith("is") || n.startsWith("has")) return true;
65
- if (n.startsWith("show") || n.startsWith("hide")) return true;
66
- return false;
67
- }
68
-
69
- function defaultStringForProp(key: string): string {
70
- if (key === "href") return "#!/governance";
71
- const k = key.toLowerCase();
72
- if (
73
- k === "title" ||
74
- k === "label" ||
75
- k === "text" ||
76
- k === "name" ||
77
- k === "heading"
78
- ) {
79
- return "Label";
80
- }
81
- return key;
82
- }
83
-
84
- function controlsFromDeclaredProps(
85
- declaredProps: string[],
86
- propKinds?: Partial<Record<string, DeclaredPropKind>>,
87
- ): PlaygroundControl[] {
88
- const skip = new Set(["key", "ref"]);
89
- const out: PlaygroundControl[] = [];
90
- for (const key of declaredProps) {
91
- if (skip.has(key)) continue;
92
- if (key === "children") {
93
- out.push({
94
- key: "children",
95
- label: "children",
96
- type: "string",
97
- default: "",
98
- placeholder: "Preview if empty",
99
- });
100
- continue;
101
- }
102
- const kind = propKinds?.[key];
103
- if (kind === "boolean") {
104
- out.push({ key, label: key, type: "boolean", default: false });
105
- } else if (kind === "number") {
106
- out.push({ key, label: key, type: "number", default: 0 });
107
- } else if (kind === "string") {
108
- out.push({
109
- key,
110
- label: key,
111
- type: "string",
112
- default: defaultStringForProp(key),
113
- placeholder: key,
114
- });
115
- } else if (isLikelyBooleanProp(key)) {
116
- out.push({ key, label: key, type: "boolean", default: false });
117
- } else {
118
- out.push({
119
- key,
120
- label: key,
121
- type: "string",
122
- default: defaultStringForProp(key),
123
- placeholder: key,
124
- });
125
- }
126
- }
127
- return out;
128
- }
129
-
130
- function controlsForSpec(
131
- catalogId: string,
132
- declaredProps: string[],
133
- propKinds: Partial<Record<string, DeclaredPropKind>> | undefined,
134
- controlOverrides: Record<string, PlaygroundControl[]>,
135
- ): PlaygroundControl[] {
136
- const override = controlOverrides[catalogId];
137
- if (override) return override;
138
- return controlsFromDeclaredProps(declaredProps, propKinds);
139
- }
140
-
141
- function valuesToComponentProps(
142
- declaredProps: string[],
143
- values: PlaygroundArgs,
144
- propKinds?: Partial<Record<string, DeclaredPropKind>>,
145
- ): Record<string, unknown> {
146
- const o: Record<string, unknown> = {};
147
- for (const key of declaredProps) {
148
- if (key === "key" || key === "ref") continue;
149
- if (key === "children") {
150
- const v = values.children;
151
- o[key] =
152
- v !== undefined && v !== null && String(v).length > 0
153
- ? String(v)
154
- : "Preview";
155
- continue;
156
- }
157
- const kind = propKinds?.[key];
158
- if (kind === "boolean") {
159
- o[key] = Boolean(values[key]);
160
- continue;
161
- }
162
- if (kind === "number") {
163
- const raw = values[key];
164
- const n = typeof raw === "number" ? raw : Number(raw);
165
- o[key] = Number.isFinite(n) ? n : 0;
166
- continue;
167
- }
168
- if (kind === "string") {
169
- o[key] = values[key];
170
- continue;
171
- }
172
- if (isLikelyBooleanProp(key)) {
173
- o[key] = Boolean(values[key]);
174
- continue;
175
- }
176
- o[key] = values[key];
177
- }
178
- return o;
179
- }
180
-
181
- function mergeStaticDefaults(
182
- fromValues: Record<string, unknown>,
183
- staticDefaults: Record<string, unknown>,
184
- ): Record<string, unknown> {
185
- const o = { ...fromValues };
186
- for (const [k, v] of Object.entries(staticDefaults)) {
187
- const cur = o[k];
188
- if (cur === undefined || cur === "") o[k] = v;
189
- }
190
- return o;
191
- }
192
-
193
- function getExport(
194
- mod: Record<string, unknown>,
195
- exportName: string,
196
- ): ComponentType<Record<string, unknown>> | undefined {
197
- const x = mod[exportName];
198
- if (typeof x === "function")
199
- return x as ComponentType<Record<string, unknown>>;
200
- return undefined;
201
- }
202
-
203
- function jsxTextOrStringifyExpression(text: string): string {
204
- if (!/[<>{}&]/.test(text)) return text;
205
- return `{JSON.stringify(${JSON.stringify(text)})}`;
206
- }
207
-
208
- function valueMatchesPlaygroundDefault(
209
- control: PlaygroundControl,
210
- value: string | number | boolean | undefined,
211
- ): boolean {
212
- switch (control.type) {
213
- case "boolean":
214
- return Boolean(value) === control.default;
215
- case "number": {
216
- const n = typeof value === "number" ? value : Number(value);
217
- return Number.isFinite(n) && n === control.default;
218
- }
219
- case "string":
220
- case "select":
221
- return String(value ?? "") === String(control.default);
222
- default:
223
- return false;
224
- }
225
- }
226
-
227
- function genericUsageSnippet(
228
- exportName: string,
229
- values: PlaygroundArgs,
230
- controls: PlaygroundControl[],
231
- ): string {
232
- const controlByKey = new Map(controls.map((c) => [c.key, c] as const));
233
-
234
- const emitPropKey = (key: string): boolean => {
235
- const c = controlByKey.get(key);
236
- if (!c) return true;
237
- return !valueMatchesPlaygroundDefault(c, values[key]);
238
- };
239
-
240
- const hasChildrenKey = Object.prototype.hasOwnProperty.call(
241
- values,
242
- "children",
243
- );
244
- const childVal = hasChildrenKey ? values.children : undefined;
245
-
246
- const propKeys = Object.keys(values)
247
- .filter((k) => k !== "children")
248
- .filter(emitPropKey)
249
- .sort((a, b) => a.localeCompare(b));
250
- const propsStr = propKeys
251
- .map((k) => `${k}={${JSON.stringify(values[k])}}`)
252
- .join(" ");
253
-
254
- const openWithProps =
255
- propKeys.length === 0 ? `<${exportName}` : `<${exportName} ${propsStr}`;
256
-
257
- if (!hasChildrenKey) {
258
- return propKeys.length === 0 ? `<${exportName} />` : `${openWithProps} />`;
259
- }
260
-
261
- if (typeof childVal === "boolean") {
262
- const allKeys = Object.keys(values)
263
- .filter(emitPropKey)
264
- .sort((a, b) => a.localeCompare(b));
265
- const allProps = allKeys
266
- .map((k) => `${k}={${JSON.stringify(values[k])}}`)
267
- .join(" ");
268
- return allKeys.length === 0
269
- ? `<${exportName} />`
270
- : `<${exportName} ${allProps} />`;
271
- }
272
-
273
- const asText =
274
- typeof childVal === "number" ? String(childVal) : String(childVal ?? "");
275
- if (asText.length === 0) {
276
- return propKeys.length === 0 ? `<${exportName} />` : `${openWithProps} />`;
277
- }
278
-
279
- const body = jsxTextOrStringifyExpression(asText);
280
- return propKeys.length === 0
281
- ? `<${exportName}>${body}</${exportName}>`
282
- : `${openWithProps}>${body}</${exportName}>`;
283
- }
284
-
285
141
  /** Sidebar / URL id — matches catalog component names (`export_name`). */
286
142
  export function playgroundCatalogId(spec: PlaygroundSpec): string {
287
143
  return spec.export_name;
@@ -310,47 +166,69 @@ export function buildPlaygroundEntriesFromReportWithSkips(
310
166
  options: BuildPlaygroundOptions = {},
311
167
  ): BuildPlaygroundResult {
312
168
  const specs = report?.playgrounds;
313
- if (!specs?.length) return { entries: [], skipped: [] };
314
-
315
169
  const globKeyFromRelPath =
316
- options.globKeyFromRelPath ?? defaultGlobKeyFromRelPath;
170
+ options.globKeyFromRelPath ?? defaultEmbedGlobKeyFromRelPath;
317
171
  const controlOverrides = options.controlOverrides ?? {};
318
172
  const staticDefaultsMap = options.staticDefaults ?? {};
319
173
 
320
- const skipped = diagnosePlaygroundJoinSkips(report, modules, {
321
- globKeyFromRelPath,
322
- });
323
- const shouldLog =
324
- options.logJoinSkips ??
325
- (typeof import.meta !== "undefined" && Boolean(import.meta.env?.DEV));
326
- if (shouldLog) logPlaygroundJoinSkips(skipped);
174
+ const skipped = specs?.length
175
+ ? diagnosePlaygroundJoinSkips(report, modules, {
176
+ globKeyFromRelPath,
177
+ })
178
+ : [];
179
+ if (options.logJoinSkips) logPlaygroundJoinSkips(skipped);
180
+
181
+ const autoEntries: PlaygroundEntry[] = [];
182
+ if (!specs?.length) {
183
+ const manualOnly = enrichManualEntriesFromReport(
184
+ collectDefinedPlaygrounds(modules),
185
+ modules,
186
+ report,
187
+ );
188
+ return { entries: mergePlaygroundEntries([], manualOnly), skipped };
189
+ }
327
190
 
328
- const out: PlaygroundEntry[] = [];
329
191
  for (const spec of specs) {
330
192
  const globKey = globKeyFromRelPath(spec.rel_path);
331
- const mod = modules[globKey];
193
+ const resolvedKey = resolveModuleKeyForRelPath(spec.rel_path, modules, globKeyFromRelPath);
194
+ const mod = resolvedKey ? modules[resolvedKey] : undefined;
332
195
  if (!mod) continue;
333
- const Cmp = getExport(mod, spec.export_name);
196
+ const Cmp = getModuleExport(mod, spec.export_name);
334
197
  if (!Cmp) continue;
335
198
 
336
199
  const catalogId = playgroundCatalogId(spec);
337
200
  const declared = spec.declared_props ?? [];
338
201
  const propKinds = normalizedPropKinds(spec.declared_prop_kinds);
339
- const controls = controlsForSpec(
340
- catalogId,
341
- declared,
342
- propKinds,
343
- controlOverrides,
202
+ const repoUsage = usageForExportName(report, spec.export_name);
203
+ const controls = ensureChildrenControl(
204
+ controlsForSpec(
205
+ catalogId,
206
+ declared,
207
+ propKinds,
208
+ spec.declared_prop_options,
209
+ spec.declared_prop_defaults,
210
+ controlOverrides,
211
+ spec.export_name,
212
+ ),
213
+ componentAcceptsChildren(declared, repoUsage),
214
+ spec.export_name,
344
215
  );
345
- const staticDefaults =
346
- staticDefaultsMap[catalogId] ??
347
- staticDefaultsMap[spec.id] ??
348
- {};
349
-
350
- function Preview({ values }: { values: PlaygroundArgs }) {
351
- const fromValues = valuesToComponentProps(declared, values, propKinds);
216
+ const staticDefaults = staticDefaultsMap[catalogId] ?? staticDefaultsMap[spec.id] ?? {};
217
+
218
+ const renderPreview = (values: PlaygroundArgs) => {
219
+ const fromValues = valuesToComponentProps(
220
+ controls,
221
+ declared,
222
+ values,
223
+ propKinds,
224
+ spec.export_name,
225
+ );
352
226
  const merged = mergeStaticDefaults(fromValues, staticDefaults);
353
227
  return createElement(Cmp, merged);
228
+ };
229
+
230
+ function Preview({ values }: { values: PlaygroundArgs }) {
231
+ return renderPreview(values);
354
232
  }
355
233
 
356
234
  const meta: PlaygroundMeta = {
@@ -359,24 +237,68 @@ export function buildPlaygroundEntriesFromReportWithSkips(
359
237
  ...(spec.group ? { group: spec.group } : {}),
360
238
  };
361
239
 
362
- out.push({
240
+ autoEntries.push({
363
241
  id: catalogId,
364
242
  meta,
365
- modulePath: globKey,
243
+ modulePath: resolvedKey ?? globKey,
366
244
  controls,
367
- usageSnippet: (values) =>
368
- genericUsageSnippet(spec.export_name, values, controls),
245
+ usageSnippet: (values) => genericUsageSnippet(spec.export_name, values, controls),
246
+ renderPreview,
369
247
  Preview: Preview as PlaygroundPreviewComponent,
370
248
  });
371
249
  }
372
250
 
373
- out.sort((a, b) => {
374
- const ga = a.meta.group ?? "";
375
- const gb = b.meta.group ?? "";
376
- if (ga !== gb) return ga.localeCompare(gb);
377
- return a.meta.title.localeCompare(b.meta.title);
251
+ if (report) {
252
+ upgradeRootEntriesWithCompoundPreview(autoEntries, report, modules, {
253
+ globKeyFromRelPath,
254
+ controlOverrides,
255
+ staticDefaults: staticDefaultsMap,
256
+ });
257
+ }
258
+
259
+ const compoundEntries = buildCompoundPlaygroundEntries(report, modules, {
260
+ globKeyFromRelPath,
261
+ controlOverrides,
262
+ staticDefaults: staticDefaultsMap,
263
+ existingIds: new Set(autoEntries.map((entry) => entry.id)),
378
264
  });
379
- return { entries: out, skipped };
265
+ const manualEntries = enrichManualEntriesFromReport(
266
+ collectDefinedPlaygrounds(modules),
267
+ modules,
268
+ report,
269
+ );
270
+ const merged = mergePlaygroundEntries(
271
+ [...autoEntries, ...compoundEntries],
272
+ manualEntries,
273
+ );
274
+ return {
275
+ entries: filterCatalogVisiblePlaygroundEntries(report, merged),
276
+ skipped,
277
+ };
278
+ }
279
+
280
+ function entryPathsForCatalog(
281
+ report: WorkspaceReport | null | undefined,
282
+ entry: PlaygroundEntry,
283
+ ): string[] {
284
+ if (!report) return [entry.modulePath];
285
+ const fromReport = definitionPathsForName(report, entry.meta.id);
286
+ return fromReport.length > 0 ? fromReport : [entry.modulePath];
287
+ }
288
+
289
+ function filterCatalogVisiblePlaygroundEntries(
290
+ report: WorkspaceReport | null | undefined,
291
+ entries: PlaygroundEntry[],
292
+ ): PlaygroundEntry[] {
293
+ if (!report) return entries;
294
+ return entries.filter(
295
+ (entry) =>
296
+ !isCatalogComponentHidden(
297
+ entry.meta.id,
298
+ report,
299
+ entryPathsForCatalog(report, entry),
300
+ ),
301
+ );
380
302
  }
381
303
 
382
304
  /** Build playground entries from report specs + eager Vite modules. */
@@ -385,6 +307,5 @@ export function buildPlaygroundEntriesFromReport(
385
307
  modules: BuildPlaygroundModules,
386
308
  options: BuildPlaygroundOptions = {},
387
309
  ): PlaygroundEntry[] {
388
- return buildPlaygroundEntriesFromReportWithSkips(report, modules, options)
389
- .entries;
310
+ return buildPlaygroundEntriesFromReportWithSkips(report, modules, options).entries;
390
311
  }
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { catalogIdFromPlaygroundExport } from "./catalogIdFromPlaygroundExport";
3
+
4
+ describe("catalogIdFromPlaygroundExport", () => {
5
+ it("maps *Playground export names to PascalCase catalog ids", () => {
6
+ expect(catalogIdFromPlaygroundExport("alertPlayground")).toBe("Alert");
7
+ expect(catalogIdFromPlaygroundExport("toggleGroupPlayground")).toBe("ToggleGroup");
8
+ expect(catalogIdFromPlaygroundExport("dropdownMenuPlayground")).toBe("DropdownMenu");
9
+ });
10
+
11
+ it("returns undefined for non-playground export names", () => {
12
+ expect(catalogIdFromPlaygroundExport("Alert")).toBeUndefined();
13
+ expect(catalogIdFromPlaygroundExport("playground")).toBeUndefined();
14
+ });
15
+ });
@@ -0,0 +1,8 @@
1
+ /** `alertPlayground` → `Alert`, `toggleGroupPlayground` → `ToggleGroup`. */
2
+ export function catalogIdFromPlaygroundExport(exportName: string): string | undefined {
3
+ const match = /^(.+)Playground$/.exec(exportName);
4
+ if (!match) return undefined;
5
+ const stem = match[1];
6
+ if (!stem) return undefined;
7
+ return stem.charAt(0).toUpperCase() + stem.slice(1);
8
+ }
@@ -0,0 +1,59 @@
1
+ import { createElement } from "react";
2
+ import { describe, expect, it } from "vitest";
3
+ import { definePlayground } from "./definePlayground";
4
+ import { collectDefinedPlaygrounds } from "./collectDefinedPlaygrounds";
5
+
6
+ describe("collectDefinedPlaygrounds", () => {
7
+ it("collects definePlayground exports from eager modules", () => {
8
+ const defined = definePlayground(
9
+ () => createElement("span", null, "menu"),
10
+ { id: "DropdownMenu", group: "ui" },
11
+ );
12
+ const modules = {
13
+ "../components/ui/dropdown-menu.playground.tsx": {
14
+ dropdownMenuPlayground: defined,
15
+ unrelated: 42,
16
+ },
17
+ };
18
+ const entries = collectDefinedPlaygrounds(modules);
19
+ expect(entries).toHaveLength(1);
20
+ expect(entries[0]?.id).toBe("DropdownMenu");
21
+ expect(entries[0]?.meta.group).toBe("ui");
22
+ expect(entries[0]?.controls).toEqual([]);
23
+ });
24
+
25
+ it("ignores modules without definePlayground exports", () => {
26
+ const modules = {
27
+ "../components/ui/button.tsx": {
28
+ Button: () => createElement("button"),
29
+ },
30
+ };
31
+ expect(collectDefinedPlaygrounds(modules)).toEqual([]);
32
+ });
33
+
34
+ it("infers catalog id from *Playground export name when id is omitted", () => {
35
+ const defined = definePlayground(() => createElement("span", null, "alert"));
36
+ const modules = {
37
+ "../components/ui/alert.playground.tsx": {
38
+ alertPlayground: defined,
39
+ },
40
+ };
41
+ const entries = collectDefinedPlaygrounds(modules);
42
+ expect(entries).toHaveLength(1);
43
+ expect(entries[0]?.id).toBe("Alert");
44
+ expect(entries[0]?.meta.title).toBe("Alert");
45
+ });
46
+
47
+ it("ignores malformed definePlayground-like exports", () => {
48
+ const modules = {
49
+ "../components/ui/dropdown-menu.playground.tsx": {
50
+ invalid: {
51
+ playgroundMeta: { id: "DropdownMenu", title: 123 },
52
+ playgroundControls: [],
53
+ PlaygroundPreview: () => createElement("span", null, "menu"),
54
+ },
55
+ },
56
+ };
57
+ expect(collectDefinedPlaygrounds(modules)).toEqual([]);
58
+ });
59
+ });