dslinter 0.1.13 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +50 -29
- package/bin/dslinter.mjs +26 -5
- package/bin/lib/config-hide-component.mjs +44 -0
- package/bin/lib/config-hide-component.test.mjs +33 -0
- package/bin/lib/constants.mjs +20 -0
- package/bin/lib/dev-banner.mjs +16 -51
- package/bin/lib/dev-banner.test.mjs +20 -18
- package/bin/lib/enrich-playgrounds-from-ts.mjs +201 -0
- package/bin/lib/enrich-playgrounds-from-ts.test.mjs +74 -0
- package/bin/lib/enrich-report-cli.mjs +14 -0
- package/bin/lib/env.mjs +20 -0
- package/bin/lib/infer-prop-types-from-ts.mjs +381 -0
- package/bin/lib/infer-prop-types-from-ts.test.mjs +174 -0
- package/bin/lib/parse-args.mjs +13 -1
- package/bin/lib/parse-args.test.mjs +7 -1
- package/bin/lib/paths.mjs +8 -0
- package/bin/lib/project-root.mjs +72 -10
- package/bin/lib/project-root.test.mjs +32 -1
- package/bin/lib/prompt.mjs +31 -0
- package/bin/lib/resolve-project.mjs +78 -0
- package/bin/lib/resolve-project.test.mjs +74 -0
- package/bin/lib/run-scanner.mjs +40 -6
- package/bin/lib/scaffold-config.mjs +128 -9
- package/bin/lib/scaffold-config.test.mjs +24 -2
- package/bin/lib/scan-host.mjs +44 -0
- package/bin/lib/scan-host.test.mjs +41 -0
- package/bin/lib/setup-readiness.mjs +153 -0
- package/bin/lib/setup-readiness.test.mjs +32 -0
- package/bin/modes/build.mjs +31 -6
- package/bin/modes/dev.mjs +55 -21
- package/bin/modes/init.mjs +3 -22
- package/bin/modes/init.test.mjs +1 -1
- package/bin/modes/mcp.mjs +49 -0
- package/bin/modes/report.mjs +29 -4
- package/bin/modes/watch.mjs +85 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bja3BuZZ.css +1 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-h0gP_iKd.js +1 -0
- package/dashboard-dist/assets/axe-DDaE9JTN.js +20 -0
- package/dashboard-dist/assets/index-B9sZ6wHm.css +1 -0
- package/dashboard-dist/assets/index-DIDBt5ed.js +218 -0
- package/dashboard-dist/index.html +2 -2
- package/index.cjs +53 -52
- package/index.d.ts +3 -0
- package/package.json +18 -12
- package/shared/env.ts +15 -0
- package/shared/paths.ts +8 -0
- package/shared/reportPath.test.ts +19 -0
- package/shared/reportPath.ts +12 -0
- package/shared/servePort.ts +16 -0
- package/src/components/ComponentInspectPane.tsx +67 -19
- package/src/components/ComponentPlaygroundPane.tsx +262 -113
- package/src/components/DashboardCommandPalette.tsx +6 -11
- package/src/components/GovernancePane.tsx +2 -2
- package/src/components/HideFromCatalogButton.tsx +44 -0
- package/src/components/OpenInEditorButton.tsx +36 -0
- package/src/components/PlaygroundA11yAndCode.tsx +53 -53
- package/src/components/PlaygroundAppThemeWrapper.tsx +82 -0
- package/src/components/PlaygroundControls.tsx +5 -11
- package/src/components/PlaygroundPreviewErrorBoundary.tsx +54 -0
- package/src/components/PlaygroundUsageCode.tsx +6 -4
- package/src/components/PlaygroundVariantMatrix.tsx +101 -34
- package/src/components/Section.tsx +5 -2
- package/src/components/Sidebar.tsx +131 -46
- package/src/components/TruncatedPath.tsx +44 -0
- package/src/components/controlApiTable.test.ts +29 -0
- package/src/components/controlApiTable.ts +3 -0
- package/src/components/playgroundUsageHighlight.ts +14 -3
- package/src/components/ui/badge.tsx +1 -1
- package/src/components/ui/table.tsx +2 -2
- package/src/dashboard/ComponentCatalog.tsx +16 -23
- package/src/dashboard/ComponentUsageDetails.tsx +6 -15
- package/src/dashboard/DashboardBody.tsx +0 -35
- package/src/dashboard/FindingsList.tsx +65 -55
- package/src/dashboard/ScannedTokenWall.tsx +3 -3
- package/src/dashboard/aggregate.test.ts +74 -0
- package/src/dashboard/aggregate.ts +145 -21
- package/src/dashboard/catalogVisibility.test.ts +93 -0
- package/src/dashboard/catalogVisibility.ts +108 -0
- package/src/dashboard/editorLink.test.ts +57 -0
- package/src/dashboard/editorLink.ts +71 -0
- package/src/dashboard/paths.test.ts +49 -0
- package/src/dashboard/paths.ts +51 -3
- package/src/dashboard/updateDslintConfig.ts +22 -0
- package/src/dashboard/useWorkspaceReport.ts +21 -17
- package/src/index.ts +26 -0
- package/src/mcp/agent-context.ts +148 -0
- package/src/mcp/agent-query.test.ts +89 -0
- package/src/mcp/agent-query.ts +373 -0
- package/src/mcp/config.ts +53 -0
- package/src/mcp/index.ts +18 -0
- package/src/mcp/normalize-paths.ts +65 -0
- package/src/mcp/report-cache.ts +212 -0
- package/src/mcp/rule-catalog.json +156 -0
- package/src/mcp/rule-catalog.ts +33 -0
- package/src/mcp/schemas.ts +54 -0
- package/src/mcp/server.test.ts +44 -0
- package/src/mcp/server.ts +343 -0
- package/src/mcp/start.ts +29 -0
- package/src/mcp/verify-loop.test.ts +49 -0
- package/src/mcp/verify-loop.ts +149 -0
- package/src/playground/appPreviewTheme.test.ts +148 -0
- package/src/playground/appPreviewTheme.ts +137 -0
- package/src/playground/buildCompoundPlaygroundEntries.test.ts +348 -0
- package/src/playground/buildCompoundPlaygroundEntries.ts +625 -0
- package/src/playground/buildPlaygroundEntriesFromReport.test.ts +420 -6
- package/src/playground/buildPlaygroundEntriesFromReport.ts +206 -285
- package/src/playground/catalogIdFromPlaygroundExport.test.ts +15 -0
- package/src/playground/catalogIdFromPlaygroundExport.ts +8 -0
- package/src/playground/collectDefinedPlaygrounds.test.ts +59 -0
- package/src/playground/collectDefinedPlaygrounds.ts +68 -0
- package/src/playground/controls.ts +177 -0
- package/src/playground/createPlaygroundRegistry.ts +1 -1
- package/src/playground/definePlayground.tsx +88 -16
- package/src/playground/definePlaygroundFromKit.ts +17 -0
- package/src/playground/embedGlobKey.ts +8 -0
- package/src/playground/enrichKitControls.test.ts +25 -0
- package/src/playground/enrichKitControls.ts +197 -0
- package/src/playground/expandPlaygroundControls.test.ts +50 -0
- package/src/playground/expandPlaygroundControls.ts +97 -0
- package/src/playground/inferKitJsx.test.ts +77 -0
- package/src/playground/inferKitJsx.ts +165 -0
- package/src/playground/inferKitParams.test.ts +41 -0
- package/src/playground/inferKitParams.ts +113 -0
- package/src/playground/inferPropTypesFromTs.d.mts +47 -0
- package/src/playground/inferPropTypesFromTs.mjs +343 -0
- package/src/playground/inferPropTypesFromTs.test.ts +227 -0
- package/src/playground/inferPropTypesFromTs.ts +17 -0
- package/src/playground/mergePlaygroundEntries.test.ts +32 -0
- package/src/playground/mergePlaygroundEntries.ts +28 -0
- package/src/playground/playgroundJoin.test.ts +79 -19
- package/src/playground/playgroundJoin.ts +47 -22
- package/src/playground/playgroundModuleExport.test.ts +42 -0
- package/src/playground/playgroundModuleExport.ts +22 -0
- package/src/playground/playgroundSpecsKey.ts +8 -0
- package/src/playground/propCoerce.ts +91 -0
- package/src/playground/scanVariantA11y.test.ts +46 -0
- package/src/playground/scanVariantA11y.ts +107 -0
- package/src/playground/snippet.ts +83 -0
- package/src/playground/usePlaygroundFromReport.test.ts +18 -8
- package/src/playground/usePlaygroundFromReport.ts +3 -1
- package/src/report/a11yForModule.ts +2 -7
- package/src/report/a11yScoring.test.ts +24 -0
- package/src/report/a11yScoring.ts +17 -0
- package/src/report/index.ts +6 -0
- package/src/shell/DashboardLayout.tsx +71 -45
- package/src/shell/DashboardLayoutAuto.tsx +0 -4
- package/src/shell/hashRoute.test.ts +7 -15
- package/src/shell/hashRoute.ts +31 -31
- package/src/shell/useHashRoute.ts +38 -13
- package/src/styles/dashboard-theme.css +18 -7
- package/src/types/controls.ts +11 -0
- package/src/types/playground.ts +4 -0
- package/src/types/report.ts +32 -9
- package/templates/playground/buildRegistry.ts +1 -1
- package/templates/vite.dslinter.snippet.ts +15 -4
- package/vite/collectScanModules.test.ts +91 -3
- package/vite/collectScanModules.ts +94 -29
- package/vite/consumer.config.mjs +6 -3
- package/vite/consumerAlias.test.ts +47 -0
- package/vite/consumerAlias.ts +114 -0
- package/vite/embedTailwindSources.test.ts +74 -0
- package/vite/embedTailwindSources.ts +97 -0
- package/vite/loadConsumerAliases.test.ts +131 -0
- package/vite/loadConsumerAliases.ts +155 -0
- package/vite/openFileInEditor.mjs +196 -0
- package/vite/openFileInEditor.test.mjs +87 -0
- package/vite/plugin.resolve.test.ts +72 -0
- package/vite/plugin.ts +216 -19
- package/vite/reportPath.test.ts +19 -0
- package/vite/resolveWayfinderImport.ts +56 -0
- package/vite/shims/inertia-react.tsx +85 -0
- package/vite/shims/wayfinder-actions.ts +33 -0
- package/vite/shims/wayfinder-routes.ts +30 -0
- package/vite/shims/ziggy-js.ts +12 -0
- package/dashboard-dist/assets/DashboardLayoutAuto-Bm7yfyC-.css +0 -1
- package/dashboard-dist/assets/DashboardLayoutAuto-DgwO_itB.js +0 -1
- package/dashboard-dist/assets/index-Cbv7vXvH.css +0 -1
- package/dashboard-dist/assets/index-e20cwqnb.js +0 -206
- package/src/components/playgroundUsageTwoslash.ts +0 -69
- package/templates/vite.dslint-scan-alias.snippet.ts +0 -4
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { createElement, type ReactNode } from "react";
|
|
2
|
+
import type { PlaygroundArgs, PlaygroundControl } from "../types/controls";
|
|
3
|
+
import type {
|
|
4
|
+
ComponentDefinition,
|
|
5
|
+
DefinitionKind,
|
|
6
|
+
PlaygroundSpec,
|
|
7
|
+
UsageSummary,
|
|
8
|
+
WorkspaceReport,
|
|
9
|
+
} from "../types/report";
|
|
10
|
+
import type { PlaygroundEntry, PlaygroundMeta } from "../types/playground";
|
|
11
|
+
import type { PlaygroundPreviewComponent } from "../types/preview";
|
|
12
|
+
import type { BuildPlaygroundModules } from "./buildPlaygroundEntriesFromReport";
|
|
13
|
+
import { getModuleExport } from "./playgroundModuleExport";
|
|
14
|
+
import { resolveModuleKeyForRelPath } from "./playgroundJoin";
|
|
15
|
+
import {
|
|
16
|
+
childrenControl,
|
|
17
|
+
childrenPropForPreview,
|
|
18
|
+
isPassthroughStringProp,
|
|
19
|
+
SKIP_PLAYGROUND_PROPS,
|
|
20
|
+
stringDefaultForProp,
|
|
21
|
+
} from "./controls";
|
|
22
|
+
|
|
23
|
+
const PLAYABLE_KINDS = new Set<DefinitionKind>([
|
|
24
|
+
"function",
|
|
25
|
+
"class",
|
|
26
|
+
"const_arrow",
|
|
27
|
+
"const_function",
|
|
28
|
+
"wrapped_component",
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const CONTENT_SUFFIXES = ["Menu", "Content", "Panel", "Body", "Viewport"] as const;
|
|
32
|
+
|
|
33
|
+
export type CompoundFamily = {
|
|
34
|
+
relPath: string;
|
|
35
|
+
group?: string;
|
|
36
|
+
root: string;
|
|
37
|
+
trigger?: string;
|
|
38
|
+
content?: string;
|
|
39
|
+
/** Playable export name → definition (first wins on duplicates). */
|
|
40
|
+
exports: Map<string, ComponentDefinition>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type BuildCompoundPlaygroundOptions = {
|
|
44
|
+
globKeyFromRelPath: (relPath: string) => string;
|
|
45
|
+
controlOverrides?: Record<string, PlaygroundControl[]>;
|
|
46
|
+
staticDefaults?: Record<string, Record<string, unknown>>;
|
|
47
|
+
/** Catalog ids that already have a playground entry. */
|
|
48
|
+
existingIds: ReadonlySet<string>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function isPlayableDefinition(def: ComponentDefinition): boolean {
|
|
52
|
+
return PLAYABLE_KINDS.has(def.kind);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fileStemFromRelPath(relPath: string): string {
|
|
56
|
+
const base = relPath.split("/").pop() ?? relPath;
|
|
57
|
+
return base.replace(/\.(tsx|jsx)$/i, "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizedName(value: string): string {
|
|
61
|
+
return value
|
|
62
|
+
.replace(/\.playground$/i, "")
|
|
63
|
+
.split("")
|
|
64
|
+
.filter((c) => c !== "-" && c !== "_")
|
|
65
|
+
.join("")
|
|
66
|
+
.toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function trimLeadingSlashes(value: string): string {
|
|
70
|
+
let i = 0;
|
|
71
|
+
while (i < value.length && value.charCodeAt(i) === 47) i += 1;
|
|
72
|
+
return value.slice(i);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function trimTrailingSlashes(value: string): string {
|
|
76
|
+
let end = value.length;
|
|
77
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
78
|
+
return value.slice(0, end);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function relPathFromAbsolute(filePath: string, reportRoot: string): string {
|
|
82
|
+
const normalizedRoot = trimTrailingSlashes(reportRoot);
|
|
83
|
+
if (filePath.startsWith(normalizedRoot + "/")) {
|
|
84
|
+
return filePath.slice(normalizedRoot.length + 1);
|
|
85
|
+
}
|
|
86
|
+
if (filePath.startsWith(normalizedRoot)) {
|
|
87
|
+
return trimLeadingSlashes(filePath.slice(normalizedRoot.length));
|
|
88
|
+
}
|
|
89
|
+
return trimLeadingSlashes(filePath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Group playable definitions by scanner rel_path. */
|
|
93
|
+
export function groupPlayableDefinitionsByFile(
|
|
94
|
+
report: WorkspaceReport,
|
|
95
|
+
): Map<string, Map<string, ComponentDefinition>> {
|
|
96
|
+
const byFile = new Map<string, Map<string, ComponentDefinition>>();
|
|
97
|
+
for (const file of report.files ?? []) {
|
|
98
|
+
const rel = relPathFromAbsolute(file.path, report.root);
|
|
99
|
+
for (const def of file.definitions ?? []) {
|
|
100
|
+
if (!isPlayableDefinition(def)) continue;
|
|
101
|
+
let exports = byFile.get(rel);
|
|
102
|
+
if (!exports) {
|
|
103
|
+
exports = new Map();
|
|
104
|
+
byFile.set(rel, exports);
|
|
105
|
+
}
|
|
106
|
+
if (!exports.has(def.name)) exports.set(def.name, def);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return byFile;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findTriggerExport(
|
|
113
|
+
stem: string,
|
|
114
|
+
root: string,
|
|
115
|
+
names: Iterable<string>,
|
|
116
|
+
): string | undefined {
|
|
117
|
+
const preferred = `${stem}Trigger`;
|
|
118
|
+
const list = [...names];
|
|
119
|
+
if (list.includes(preferred)) return preferred;
|
|
120
|
+
return list.find((n) => n !== root && n.endsWith("Trigger"));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findContentExport(
|
|
124
|
+
stem: string,
|
|
125
|
+
root: string,
|
|
126
|
+
trigger: string | undefined,
|
|
127
|
+
names: Iterable<string>,
|
|
128
|
+
): string | undefined {
|
|
129
|
+
const candidates = [...names].filter((n) => n !== root && n !== trigger);
|
|
130
|
+
for (const suffix of CONTENT_SUFFIXES) {
|
|
131
|
+
const preferred = `${stem}${suffix}`;
|
|
132
|
+
if (candidates.includes(preferred)) return preferred;
|
|
133
|
+
}
|
|
134
|
+
return candidates.find((n) => CONTENT_SUFFIXES.some((suffix) => n.endsWith(suffix)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Detect a stem-rooted compound component family in one module. */
|
|
138
|
+
export function detectCompoundFamily(
|
|
139
|
+
relPath: string,
|
|
140
|
+
exports: Map<string, ComponentDefinition>,
|
|
141
|
+
group?: string,
|
|
142
|
+
): CompoundFamily | null {
|
|
143
|
+
if (exports.size < 2) return null;
|
|
144
|
+
const stem = fileStemFromRelPath(relPath);
|
|
145
|
+
const root = [...exports.keys()].find((name) => normalizedName(name) === normalizedName(stem));
|
|
146
|
+
if (!root) return null;
|
|
147
|
+
|
|
148
|
+
const names = [...exports.keys()];
|
|
149
|
+
const trigger = findTriggerExport(root, root, names);
|
|
150
|
+
const content = findContentExport(root, root, trigger, names);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
relPath,
|
|
154
|
+
group,
|
|
155
|
+
root,
|
|
156
|
+
trigger,
|
|
157
|
+
content,
|
|
158
|
+
exports,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function playgroundGroupForFile(report: WorkspaceReport, relPath: string): string | undefined {
|
|
163
|
+
const spec = report.playgrounds?.find((p) => p.rel_path === relPath);
|
|
164
|
+
return spec?.group ?? undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** All compound families in a workspace report. */
|
|
168
|
+
export function findCompoundFamilies(report: WorkspaceReport): CompoundFamily[] {
|
|
169
|
+
const byFile = groupPlayableDefinitionsByFile(report);
|
|
170
|
+
const families: CompoundFamily[] = [];
|
|
171
|
+
for (const [relPath, exports] of byFile) {
|
|
172
|
+
const family = detectCompoundFamily(relPath, exports, playgroundGroupForFile(report, relPath));
|
|
173
|
+
if (family) families.push(family);
|
|
174
|
+
}
|
|
175
|
+
return families;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function usageForExportName(report: WorkspaceReport, exportName: string): UsageSummary | undefined {
|
|
179
|
+
return report.usage_by_component?.find((u) => u.component === exportName);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isLikelyBooleanProp(name: string): boolean {
|
|
183
|
+
const n = name.toLowerCase();
|
|
184
|
+
if (n === "disabled" || n === "loading" || n === "aschild") return true;
|
|
185
|
+
if (n.startsWith("is") || n.startsWith("has")) return true;
|
|
186
|
+
if (n.startsWith("show") || n.startsWith("hide")) return true;
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function controlsFromDefinitionAndUsage(
|
|
191
|
+
def: ComponentDefinition,
|
|
192
|
+
usage: UsageSummary | undefined,
|
|
193
|
+
controlOverrides: Record<string, PlaygroundControl[]>,
|
|
194
|
+
catalogId: string,
|
|
195
|
+
): PlaygroundControl[] {
|
|
196
|
+
const override = controlOverrides[catalogId];
|
|
197
|
+
if (override) return override;
|
|
198
|
+
|
|
199
|
+
const declared = def.declared_props ?? [];
|
|
200
|
+
const skip = new Set([...SKIP_PLAYGROUND_PROPS, "as", "asChild"]);
|
|
201
|
+
const propKeys = new Set<string>(declared);
|
|
202
|
+
|
|
203
|
+
// Include props observed in usage when the scanner did not declare them.
|
|
204
|
+
if (usage?.prop_frequencies) {
|
|
205
|
+
for (const key of Object.keys(usage.prop_frequencies)) {
|
|
206
|
+
if (!skip.has(key)) propKeys.add(key);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const out: PlaygroundControl[] = [];
|
|
211
|
+
|
|
212
|
+
for (const key of [...propKeys].sort((a, b) => a.localeCompare(b))) {
|
|
213
|
+
if (skip.has(key)) continue;
|
|
214
|
+
if (key === "children") {
|
|
215
|
+
out.push({ ...childrenControl(catalogId), defaultSource: "example" });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const options = def.declared_prop_options?.[key];
|
|
220
|
+
if (options && options.length >= 2) {
|
|
221
|
+
const defaultVal =
|
|
222
|
+
def.declared_prop_defaults?.[key] ??
|
|
223
|
+
(options.includes("default") ? "default" : options[0]!);
|
|
224
|
+
out.push({
|
|
225
|
+
key,
|
|
226
|
+
label: key,
|
|
227
|
+
type: "select",
|
|
228
|
+
default: defaultVal,
|
|
229
|
+
defaultSource: def.declared_prop_defaults?.[key] == null ? "example" : "type",
|
|
230
|
+
options: options.map((value) => ({ value, label: value })),
|
|
231
|
+
});
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const freqValues = usage?.prop_value_frequencies?.[key];
|
|
236
|
+
if (freqValues && Object.keys(freqValues).length >= 2) {
|
|
237
|
+
const sorted = Object.entries(freqValues).sort((a, b) => b[1] - a[1]);
|
|
238
|
+
const values = sorted.map(([v]) => v);
|
|
239
|
+
out.push({
|
|
240
|
+
key,
|
|
241
|
+
label: key,
|
|
242
|
+
type: "select",
|
|
243
|
+
default: values[0]!,
|
|
244
|
+
defaultSource: "example",
|
|
245
|
+
options: values.map((value) => ({ value, label: value })),
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (isLikelyBooleanProp(key)) {
|
|
251
|
+
out.push({ key, label: key, type: "boolean", default: false, defaultSource: "example" });
|
|
252
|
+
} else {
|
|
253
|
+
out.push({
|
|
254
|
+
key,
|
|
255
|
+
label: key,
|
|
256
|
+
type: "string",
|
|
257
|
+
default: stringDefaultForProp(key),
|
|
258
|
+
defaultSource: "example",
|
|
259
|
+
placeholder: isPassthroughStringProp(key) ? undefined : key,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (
|
|
265
|
+
!out.some((c) => c.key === "children") &&
|
|
266
|
+
(propKeys.has("children") || (usage?.prop_frequencies?.children ?? 0) > 0)
|
|
267
|
+
) {
|
|
268
|
+
out.push({ ...childrenControl(catalogId), defaultSource: "example" });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function valuesToProps(
|
|
275
|
+
controls: PlaygroundControl[],
|
|
276
|
+
values: PlaygroundArgs,
|
|
277
|
+
exportName?: string,
|
|
278
|
+
): Record<string, unknown> {
|
|
279
|
+
const o: Record<string, unknown> = {};
|
|
280
|
+
for (const control of controls) {
|
|
281
|
+
const key = control.key;
|
|
282
|
+
if (SKIP_PLAYGROUND_PROPS.has(key)) continue;
|
|
283
|
+
if (key === "children") {
|
|
284
|
+
const coerced = childrenPropForPreview(exportName, values.children);
|
|
285
|
+
if (coerced !== undefined) o[key] = coerced;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (isPassthroughStringProp(key)) {
|
|
289
|
+
const raw = values[key];
|
|
290
|
+
if (raw === undefined || raw === null || String(raw).length === 0) continue;
|
|
291
|
+
o[key] = String(raw);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (control.type === "boolean") {
|
|
295
|
+
o[key] = Boolean(values[key]);
|
|
296
|
+
} else if (control.type === "number") {
|
|
297
|
+
const raw = values[key];
|
|
298
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
299
|
+
o[key] = Number.isFinite(n) ? n : 0;
|
|
300
|
+
} else {
|
|
301
|
+
o[key] = values[key];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return o;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mergeStaticDefaults(
|
|
308
|
+
fromValues: Record<string, unknown>,
|
|
309
|
+
staticDefaults: Record<string, unknown>,
|
|
310
|
+
): Record<string, unknown> {
|
|
311
|
+
const o = { ...fromValues };
|
|
312
|
+
for (const [k, v] of Object.entries(staticDefaults)) {
|
|
313
|
+
const cur = o[k];
|
|
314
|
+
if (cur === undefined || (cur === "" && k !== "children")) o[k] = v;
|
|
315
|
+
}
|
|
316
|
+
return o;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function placeholderPartChildren(exportName: string): ReactNode {
|
|
320
|
+
if (exportName.endsWith("Separator")) return null;
|
|
321
|
+
if (exportName.endsWith("Header") || exportName.endsWith("Label")) {
|
|
322
|
+
return "Section";
|
|
323
|
+
}
|
|
324
|
+
if (exportName.includes("Item") || exportName.endsWith("Action")) {
|
|
325
|
+
return "Example item";
|
|
326
|
+
}
|
|
327
|
+
return "Example";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function demoMenuChildren(
|
|
331
|
+
family: CompoundFamily,
|
|
332
|
+
mod: Record<string, unknown>,
|
|
333
|
+
targetName: string,
|
|
334
|
+
): ReactNode {
|
|
335
|
+
const ItemExport = [...family.exports.keys()].find((n) => n.endsWith("Item") && n !== targetName);
|
|
336
|
+
const SepExport = [...family.exports.keys()].find((n) => n.endsWith("Separator"));
|
|
337
|
+
|
|
338
|
+
const nodes: ReactNode[] = [];
|
|
339
|
+
|
|
340
|
+
if (targetName !== family.content) {
|
|
341
|
+
const Target = getModuleExport(mod, targetName);
|
|
342
|
+
if (Target) {
|
|
343
|
+
nodes.push(createElement(Target, null, placeholderPartChildren(targetName)));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (ItemExport && targetName !== ItemExport) {
|
|
348
|
+
const Item = getModuleExport(mod, ItemExport);
|
|
349
|
+
if (Item) nodes.push(createElement(Item, null, "Example item"));
|
|
350
|
+
}
|
|
351
|
+
if (SepExport && targetName !== SepExport && nodes.length > 0) {
|
|
352
|
+
const Sep = getModuleExport(mod, SepExport);
|
|
353
|
+
if (Sep) nodes.push(createElement(Sep));
|
|
354
|
+
}
|
|
355
|
+
if (ItemExport && targetName !== ItemExport && nodes.length > 1) {
|
|
356
|
+
const Item = getModuleExport(mod, ItemExport);
|
|
357
|
+
if (Item) nodes.push(createElement(Item, null, "Another item"));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (nodes.length === 0) {
|
|
361
|
+
return placeholderPartChildren(targetName);
|
|
362
|
+
}
|
|
363
|
+
return nodes;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderCompoundPreview(
|
|
367
|
+
family: CompoundFamily,
|
|
368
|
+
targetName: string,
|
|
369
|
+
mod: Record<string, unknown>,
|
|
370
|
+
props: Record<string, unknown>,
|
|
371
|
+
): ReactNode {
|
|
372
|
+
const Root = getModuleExport(mod, family.root);
|
|
373
|
+
const Target = getModuleExport(mod, targetName);
|
|
374
|
+
if (!Root || !Target) return null;
|
|
375
|
+
|
|
376
|
+
const triggerProps: Record<string, unknown> = { children: "Open menu" };
|
|
377
|
+
const Trigger = family.trigger ? getModuleExport(mod, family.trigger) : undefined;
|
|
378
|
+
|
|
379
|
+
if (targetName === family.root) {
|
|
380
|
+
return createElement(Root, props);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (targetName === family.trigger && Trigger) {
|
|
384
|
+
return createElement(
|
|
385
|
+
Root,
|
|
386
|
+
{ defaultOpen: true },
|
|
387
|
+
createElement(Trigger, { ...triggerProps, ...props }),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const triggerNode =
|
|
392
|
+
Trigger && targetName !== family.trigger ? createElement(Trigger, triggerProps) : null;
|
|
393
|
+
const rootProps = { defaultOpen: true };
|
|
394
|
+
|
|
395
|
+
const targetChildren: ReactNode =
|
|
396
|
+
props.children !== undefined
|
|
397
|
+
? (props.children as ReactNode)
|
|
398
|
+
: placeholderPartChildren(targetName);
|
|
399
|
+
|
|
400
|
+
const targetNode = createElement(Target, props, targetChildren);
|
|
401
|
+
|
|
402
|
+
if (targetName === family.content) {
|
|
403
|
+
return createElement(Root, rootProps, triggerNode, targetNode);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (family.content) {
|
|
407
|
+
const Content = getModuleExport(mod, family.content);
|
|
408
|
+
if (Content) {
|
|
409
|
+
const menuChildren =
|
|
410
|
+
targetName === family.content ? targetNode : demoMenuChildren(family, mod, targetName);
|
|
411
|
+
return createElement(
|
|
412
|
+
Root,
|
|
413
|
+
rootProps,
|
|
414
|
+
triggerNode,
|
|
415
|
+
createElement(Content, null, menuChildren),
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return createElement(Root, rootProps, triggerNode, targetNode);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function compoundUsageSnippet(
|
|
424
|
+
family: CompoundFamily,
|
|
425
|
+
targetName: string,
|
|
426
|
+
values: PlaygroundArgs,
|
|
427
|
+
controls: PlaygroundControl[],
|
|
428
|
+
): string {
|
|
429
|
+
const propsStr = controls
|
|
430
|
+
.filter((c) => c.key !== "children")
|
|
431
|
+
.map((c) => `${c.key}={${JSON.stringify(values[c.key])}}`)
|
|
432
|
+
.join(" ");
|
|
433
|
+
|
|
434
|
+
const targetOpen = propsStr.length > 0 ? `<${targetName} ${propsStr}` : `<${targetName}`;
|
|
435
|
+
|
|
436
|
+
const trigger = family.trigger ?? "button";
|
|
437
|
+
const content = family.content;
|
|
438
|
+
|
|
439
|
+
if (targetName === family.root) {
|
|
440
|
+
return propsStr.length > 0 ? `<${targetName} ${propsStr} />` : `<${targetName} />`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (targetName === family.trigger) {
|
|
444
|
+
return `<${family.root}>\n <${targetName}>Open menu</${targetName}>\n</${family.root}>`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (targetName === content) {
|
|
448
|
+
return `<${family.root}>\n <${trigger}>Open menu</${trigger}>\n ${targetOpen}>…</${targetName}>\n</${family.root}>`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (content) {
|
|
452
|
+
return `<${family.root}>\n <${trigger}>Open menu</${trigger}>\n <${content}>\n ${targetOpen}>…</${targetName}>\n </${content}>\n</${family.root}>`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return `<${family.root}>\n <${trigger}>Open menu</${trigger}>\n ${targetOpen}>…</${targetName}>\n</${family.root}>`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Primary preview target for a compound family (content, then trigger). */
|
|
459
|
+
export function primaryCompoundTarget(family: CompoundFamily): string | undefined {
|
|
460
|
+
return family.content ?? family.trigger;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function buildCompoundPlaygroundEntryForTarget(
|
|
464
|
+
family: CompoundFamily,
|
|
465
|
+
targetName: string,
|
|
466
|
+
mod: Record<string, unknown>,
|
|
467
|
+
modulePath: string,
|
|
468
|
+
report: WorkspaceReport,
|
|
469
|
+
options: Pick<
|
|
470
|
+
BuildCompoundPlaygroundOptions,
|
|
471
|
+
"controlOverrides" | "staticDefaults"
|
|
472
|
+
>,
|
|
473
|
+
): PlaygroundEntry | null {
|
|
474
|
+
const def = family.exports.get(targetName);
|
|
475
|
+
if (!def) return null;
|
|
476
|
+
if (!getModuleExport(mod, targetName)) return null;
|
|
477
|
+
|
|
478
|
+
const usage = usageForExportName(report, targetName);
|
|
479
|
+
const controls = controlsFromDefinitionAndUsage(
|
|
480
|
+
def,
|
|
481
|
+
usage,
|
|
482
|
+
options.controlOverrides ?? {},
|
|
483
|
+
targetName,
|
|
484
|
+
);
|
|
485
|
+
const staticDefaults = options.staticDefaults?.[targetName] ?? {};
|
|
486
|
+
|
|
487
|
+
const renderPreview = (values: PlaygroundArgs) => {
|
|
488
|
+
const props = mergeStaticDefaults(
|
|
489
|
+
valuesToProps(controls, values, targetName),
|
|
490
|
+
staticDefaults,
|
|
491
|
+
);
|
|
492
|
+
return renderCompoundPreview(family, targetName, mod, props);
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
function Preview({ values }: { values: PlaygroundArgs }) {
|
|
496
|
+
return renderPreview(values);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const meta: PlaygroundMeta = {
|
|
500
|
+
id: targetName,
|
|
501
|
+
title: targetName,
|
|
502
|
+
...(family.group ? { group: family.group } : {}),
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
id: targetName,
|
|
507
|
+
meta,
|
|
508
|
+
modulePath,
|
|
509
|
+
controls,
|
|
510
|
+
usageSnippet: (values) => compoundUsageSnippet(family, targetName, values, controls),
|
|
511
|
+
renderPreview,
|
|
512
|
+
Preview: Preview as PlaygroundPreviewComponent,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Synthesize compound playground entries for sub-exports (e.g. DropdownMenu)
|
|
518
|
+
* that share a stem-rooted provider module with sibling exports.
|
|
519
|
+
*/
|
|
520
|
+
export function buildCompoundPlaygroundEntries(
|
|
521
|
+
report: WorkspaceReport | null | undefined,
|
|
522
|
+
modules: BuildPlaygroundModules,
|
|
523
|
+
options: BuildCompoundPlaygroundOptions,
|
|
524
|
+
): PlaygroundEntry[] {
|
|
525
|
+
if (!report) return [];
|
|
526
|
+
|
|
527
|
+
const out: PlaygroundEntry[] = [];
|
|
528
|
+
const families = findCompoundFamilies(report);
|
|
529
|
+
|
|
530
|
+
for (const family of families) {
|
|
531
|
+
const globKey = options.globKeyFromRelPath(family.relPath);
|
|
532
|
+
const resolvedKey = resolveModuleKeyForRelPath(
|
|
533
|
+
family.relPath,
|
|
534
|
+
modules,
|
|
535
|
+
options.globKeyFromRelPath,
|
|
536
|
+
);
|
|
537
|
+
const mod = resolvedKey ? modules[resolvedKey] : undefined;
|
|
538
|
+
if (!mod) continue;
|
|
539
|
+
if (!getModuleExport(mod, family.root)) continue;
|
|
540
|
+
|
|
541
|
+
for (const exportName of family.exports.keys()) {
|
|
542
|
+
if (exportName === family.root) continue;
|
|
543
|
+
if (options.existingIds.has(exportName)) continue;
|
|
544
|
+
|
|
545
|
+
const entry = buildCompoundPlaygroundEntryForTarget(
|
|
546
|
+
family,
|
|
547
|
+
exportName,
|
|
548
|
+
mod,
|
|
549
|
+
resolvedKey ?? globKey,
|
|
550
|
+
report,
|
|
551
|
+
options,
|
|
552
|
+
);
|
|
553
|
+
if (entry) out.push(entry);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
out.sort((a, b) => a.meta.title.localeCompare(b.meta.title));
|
|
558
|
+
return out;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Replace auto-generated root entries with composed compound previews
|
|
563
|
+
* (e.g. `DropdownMenu` shows trigger + content instead of bare root).
|
|
564
|
+
*/
|
|
565
|
+
export function upgradeRootEntriesWithCompoundPreview(
|
|
566
|
+
autoEntries: PlaygroundEntry[],
|
|
567
|
+
report: WorkspaceReport,
|
|
568
|
+
modules: BuildPlaygroundModules,
|
|
569
|
+
options: Pick<
|
|
570
|
+
BuildCompoundPlaygroundOptions,
|
|
571
|
+
"globKeyFromRelPath" | "controlOverrides" | "staticDefaults"
|
|
572
|
+
>,
|
|
573
|
+
): void {
|
|
574
|
+
const families = findCompoundFamilies(report);
|
|
575
|
+
|
|
576
|
+
for (const family of families) {
|
|
577
|
+
const targetName = primaryCompoundTarget(family);
|
|
578
|
+
if (!targetName || targetName === family.root) continue;
|
|
579
|
+
|
|
580
|
+
const globKey = options.globKeyFromRelPath(family.relPath);
|
|
581
|
+
const resolvedKey = resolveModuleKeyForRelPath(
|
|
582
|
+
family.relPath,
|
|
583
|
+
modules,
|
|
584
|
+
options.globKeyFromRelPath,
|
|
585
|
+
);
|
|
586
|
+
const modulePath = resolvedKey ?? globKey;
|
|
587
|
+
const mod = resolvedKey ? modules[resolvedKey] : undefined;
|
|
588
|
+
if (!mod) continue;
|
|
589
|
+
|
|
590
|
+
const index = autoEntries.findIndex(
|
|
591
|
+
(entry) => entry.id === family.root && entry.modulePath === modulePath,
|
|
592
|
+
);
|
|
593
|
+
if (index < 0) continue;
|
|
594
|
+
|
|
595
|
+
const compoundEntry = buildCompoundPlaygroundEntryForTarget(
|
|
596
|
+
family,
|
|
597
|
+
targetName,
|
|
598
|
+
mod,
|
|
599
|
+
modulePath,
|
|
600
|
+
report,
|
|
601
|
+
options,
|
|
602
|
+
);
|
|
603
|
+
if (!compoundEntry) continue;
|
|
604
|
+
|
|
605
|
+
const existing = autoEntries[index]!;
|
|
606
|
+
autoEntries[index] = {
|
|
607
|
+
...existing,
|
|
608
|
+
controls: compoundEntry.controls,
|
|
609
|
+
renderPreview: compoundEntry.renderPreview,
|
|
610
|
+
Preview: compoundEntry.Preview,
|
|
611
|
+
usageSnippet: compoundEntry.usageSnippet,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/** @internal test helper — map rel_path → playground group from report specs. */
|
|
617
|
+
export function playgroundSpecByRelPath(
|
|
618
|
+
specs: PlaygroundSpec[] | undefined,
|
|
619
|
+
): Map<string, PlaygroundSpec> {
|
|
620
|
+
const map = new Map<string, PlaygroundSpec>();
|
|
621
|
+
for (const spec of specs ?? []) {
|
|
622
|
+
if (!map.has(spec.rel_path)) map.set(spec.rel_path, spec);
|
|
623
|
+
}
|
|
624
|
+
return map;
|
|
625
|
+
}
|