dslinter 0.1.13 → 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.
- package/CHANGELOG.md +43 -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 +96 -8
- package/bin/lib/scaffold-config.test.mjs +12 -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 +209 -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 +51 -3
- package/vite/collectScanModules.ts +85 -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
|
@@ -1,13 +1,108 @@
|
|
|
1
|
-
import { createElement
|
|
1
|
+
import { createElement } from "react";
|
|
2
2
|
import type { PlaygroundArgs, PlaygroundControl } from "../types/controls";
|
|
3
|
-
import type {
|
|
4
|
-
import type { PlaygroundEntry, PlaygroundMeta
|
|
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
|
|
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
|
|
29
|
-
|
|
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 ??
|
|
170
|
+
options.globKeyFromRelPath ?? defaultEmbedGlobKeyFromRelPath;
|
|
317
171
|
const controlOverrides = options.controlOverrides ?? {};
|
|
318
172
|
const staticDefaultsMap = options.staticDefaults ?? {};
|
|
319
173
|
|
|
320
|
-
const skipped =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
|
193
|
+
const resolvedKey = resolveModuleKeyForRelPath(spec.rel_path, modules, globKeyFromRelPath);
|
|
194
|
+
const mod = resolvedKey ? modules[resolvedKey] : undefined;
|
|
332
195
|
if (!mod) continue;
|
|
333
|
-
const Cmp =
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
240
|
+
autoEntries.push({
|
|
363
241
|
id: catalogId,
|
|
364
242
|
meta,
|
|
365
|
-
modulePath: globKey,
|
|
243
|
+
modulePath: resolvedKey ?? globKey,
|
|
366
244
|
controls,
|
|
367
|
-
usageSnippet: (values) =>
|
|
368
|
-
|
|
245
|
+
usageSnippet: (values) => genericUsageSnippet(spec.export_name, values, controls),
|
|
246
|
+
renderPreview,
|
|
369
247
|
Preview: Preview as PlaygroundPreviewComponent,
|
|
370
248
|
});
|
|
371
249
|
}
|
|
372
250
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
+
});
|