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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { expandPlaygroundControls, exampleProps, propsFromControls } from "./expandPlaygroundControls";
|
|
3
|
+
|
|
4
|
+
describe("expandPlaygroundControls", () => {
|
|
5
|
+
it("expands string shorthand to string controls", () => {
|
|
6
|
+
const controls = expandPlaygroundControls({ title: "Hello" });
|
|
7
|
+
expect(controls).toEqual([
|
|
8
|
+
{ key: "title", label: "Title", type: "string", default: "Hello" },
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("expands prop key list with prop-name defaults", () => {
|
|
13
|
+
const controls = expandPlaygroundControls(["title", "description"]);
|
|
14
|
+
expect(controls).toEqual([
|
|
15
|
+
{
|
|
16
|
+
key: "title",
|
|
17
|
+
label: "Title",
|
|
18
|
+
type: "string",
|
|
19
|
+
default: "title",
|
|
20
|
+
defaultSource: "example",
|
|
21
|
+
placeholder: "title",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: "description",
|
|
25
|
+
label: "Description",
|
|
26
|
+
type: "string",
|
|
27
|
+
default: "description",
|
|
28
|
+
defaultSource: "example",
|
|
29
|
+
placeholder: "description",
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
expect(propsFromControls(controls, {})).toEqual({
|
|
33
|
+
title: "title",
|
|
34
|
+
description: "description",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("exampleProps builds prop-name defaults", () => {
|
|
39
|
+
expect(exampleProps("title", "description")).toEqual({
|
|
40
|
+
title: "title",
|
|
41
|
+
description: "description",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("propsFromControls uses control defaults", () => {
|
|
46
|
+
const controls = expandPlaygroundControls({ count: 2, enabled: true });
|
|
47
|
+
expect(propsFromControls(controls, {})).toEqual({ count: 2, enabled: true });
|
|
48
|
+
expect(propsFromControls(controls, { count: 5 })).toEqual({ count: 5, enabled: true });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { defaultArgsFromControls } from "../types/controls";
|
|
2
|
+
import type { PlaygroundArgs, PlaygroundControl } from "../types/controls";
|
|
3
|
+
|
|
4
|
+
export type CompactPlaygroundControl =
|
|
5
|
+
| string
|
|
6
|
+
| number
|
|
7
|
+
| boolean
|
|
8
|
+
| Partial<PlaygroundControl> & { key?: string };
|
|
9
|
+
|
|
10
|
+
/** Shorthand: list prop keys; defaults and placeholders use the key name. */
|
|
11
|
+
export type PlaygroundControlKeys = readonly string[];
|
|
12
|
+
|
|
13
|
+
export type PlaygroundControlsInput =
|
|
14
|
+
| PlaygroundControl[]
|
|
15
|
+
| Record<string, CompactPlaygroundControl>
|
|
16
|
+
| PlaygroundControlKeys;
|
|
17
|
+
|
|
18
|
+
function isPropKeyList(input: readonly unknown[]): input is readonly string[] {
|
|
19
|
+
return input.length > 0 && input.every((item) => typeof item === "string");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function titleCase(key: string): string {
|
|
23
|
+
return key
|
|
24
|
+
.replace(/([A-Z])/g, " $1")
|
|
25
|
+
.replace(/^./, (c) => c.toUpperCase())
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isFullControl(value: CompactPlaygroundControl): value is PlaygroundControl {
|
|
30
|
+
return (
|
|
31
|
+
typeof value === "object" &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
"type" in value &&
|
|
34
|
+
typeof (value as PlaygroundControl).type === "string"
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function expandOne(key: string, value: CompactPlaygroundControl): PlaygroundControl {
|
|
39
|
+
if (isFullControl(value)) {
|
|
40
|
+
const label = value.label ?? titleCase(key);
|
|
41
|
+
return { ...value, key, label } as PlaygroundControl;
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "boolean") {
|
|
44
|
+
return { key, label: titleCase(key), type: "boolean", default: value };
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "number") {
|
|
47
|
+
return { key, label: titleCase(key), type: "number", default: value };
|
|
48
|
+
}
|
|
49
|
+
return { key, label: titleCase(key), type: "string", default: String(value) };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function expandPropNameControl(key: string): PlaygroundControl {
|
|
53
|
+
return {
|
|
54
|
+
key,
|
|
55
|
+
label: titleCase(key),
|
|
56
|
+
type: "string",
|
|
57
|
+
default: key,
|
|
58
|
+
defaultSource: "example",
|
|
59
|
+
placeholder: key,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Shorthand defaults where each value equals the prop key (panel placeholder text). */
|
|
64
|
+
export function exampleProps(...keys: string[]): Record<string, string> {
|
|
65
|
+
return Object.fromEntries(keys.map((key) => [key, key]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Expand shorthand `controls` into full `PlaygroundControl` objects. */
|
|
69
|
+
export function expandPlaygroundControls(
|
|
70
|
+
input: PlaygroundControlsInput | undefined,
|
|
71
|
+
): PlaygroundControl[] {
|
|
72
|
+
if (!input) return [];
|
|
73
|
+
if (Array.isArray(input)) {
|
|
74
|
+
if (isPropKeyList(input)) {
|
|
75
|
+
return input.map((key) => expandPropNameControl(key));
|
|
76
|
+
}
|
|
77
|
+
return input;
|
|
78
|
+
}
|
|
79
|
+
return Object.entries(input).map(([key, value]) => expandOne(key, value));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Map panel values to a props object using control defaults (no manual `??` fallbacks). */
|
|
83
|
+
export function propsFromControls<T extends PlaygroundArgs>(
|
|
84
|
+
controls: PlaygroundControl[],
|
|
85
|
+
values: PlaygroundArgs,
|
|
86
|
+
defaults?: Partial<T>,
|
|
87
|
+
): T {
|
|
88
|
+
const out = { ...defaultArgsFromControls(controls), ...defaults } as Record<
|
|
89
|
+
string,
|
|
90
|
+
string | number | boolean
|
|
91
|
+
>;
|
|
92
|
+
for (const c of controls) {
|
|
93
|
+
const v = values[c.key];
|
|
94
|
+
if (v !== undefined) out[c.key] = v;
|
|
95
|
+
}
|
|
96
|
+
return out as T;
|
|
97
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
inferKitJsxSlots,
|
|
5
|
+
inferKitRootPropBindings,
|
|
6
|
+
slotDefaultFromComponent,
|
|
7
|
+
slotLabelFromComponent,
|
|
8
|
+
} from "./inferKitJsx";
|
|
9
|
+
|
|
10
|
+
function Alert(props: { variant?: string; children?: unknown }) {
|
|
11
|
+
return createElement("div", props);
|
|
12
|
+
}
|
|
13
|
+
function AlertTitle(props: { children?: unknown }) {
|
|
14
|
+
return createElement("div", props);
|
|
15
|
+
}
|
|
16
|
+
function AlertDescription(props: { children?: unknown }) {
|
|
17
|
+
return createElement("div", props);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("inferKitJsx", () => {
|
|
21
|
+
it("reads createElement children bindings (including vite transforms)", () => {
|
|
22
|
+
const viteTransformed =
|
|
23
|
+
"({ title, description }) => (0,createElement)(Alert, { variant: 'default' }, (0,createElement)(AlertTitle, null, title), (0,createElement)(AlertDescription, null, description))";
|
|
24
|
+
const fromVite = Object.assign(() => null, { toString: () => viteTransformed });
|
|
25
|
+
expect(inferKitJsxSlots(fromVite)).toEqual([
|
|
26
|
+
{ component: "AlertTitle", param: "title" },
|
|
27
|
+
{ component: "AlertDescription", param: "description" },
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("reads root prop bindings from createElement", () => {
|
|
32
|
+
const fromVite = Object.assign(() => null, {
|
|
33
|
+
toString: () => "({ variant }) => (0,createElement)(Alert, { variant }, (0,createElement)(AlertTitle, null, 'Hi'))",
|
|
34
|
+
});
|
|
35
|
+
expect(inferKitRootPropBindings(fromVite)).toEqual([
|
|
36
|
+
{ component: "Alert", prop: "variant", param: "variant" },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("reads root prop bindings from vite SSR import wrappers", () => {
|
|
41
|
+
const fromVite = Object.assign(() => null, {
|
|
42
|
+
toString: () =>
|
|
43
|
+
"({ title, description, variant }) => (0,__vite_ssr_import_0__.createElement)(\n Alert,\n { variant },\n (0,__vite_ssr_import_0__.createElement)(AlertTitle, null, title),\n (0,__vite_ssr_import_0__.createElement)(AlertDescription, null, description)\n )",
|
|
44
|
+
});
|
|
45
|
+
expect(inferKitRootPropBindings(fromVite)).toEqual([
|
|
46
|
+
{ component: "Alert", prop: "variant", param: "variant" },
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reads root prop bindings from source JSX", () => {
|
|
51
|
+
const kit = Object.assign(() => null, {
|
|
52
|
+
toString: () =>
|
|
53
|
+
'({ title, description, variant }) => (<Alert className="max-w-md" variant={variant}><AlertTitle>{title}</AlertTitle></Alert>)',
|
|
54
|
+
});
|
|
55
|
+
expect(inferKitRootPropBindings(kit)).toEqual([
|
|
56
|
+
{ component: "Alert", prop: "variant", param: "variant" },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("reads source JSX slots", () => {
|
|
61
|
+
const kit = Object.assign(() => null, {
|
|
62
|
+
toString: () =>
|
|
63
|
+
"({ title, description }) => (<Alert><AlertTitle>{title}</AlertTitle><AlertDescription>{description}</AlertDescription></Alert>)",
|
|
64
|
+
});
|
|
65
|
+
expect(inferKitJsxSlots(kit)).toEqual([
|
|
66
|
+
{ component: "AlertTitle", param: "title" },
|
|
67
|
+
{ component: "AlertDescription", param: "description" },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("derives slot labels and defaults from component names", () => {
|
|
72
|
+
expect(slotLabelFromComponent("AlertTitle")).toBe("Title");
|
|
73
|
+
expect(slotLabelFromComponent("AlertDescription")).toBe("Description");
|
|
74
|
+
expect(slotDefaultFromComponent("AlertTitle")).toBe("Heads up");
|
|
75
|
+
expect(slotDefaultFromComponent("DialogTrigger")).toBe("Open");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
export type KitJsxSlot = {
|
|
2
|
+
param: string;
|
|
3
|
+
component: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type KitRootPropBinding = {
|
|
7
|
+
param: string;
|
|
8
|
+
component: string;
|
|
9
|
+
prop: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function kitSource(fn: Function): string {
|
|
13
|
+
return fn.toString();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Vite/esbuild wraps JSX as `(0, import.createElement)(Type, props)` (often split across lines). */
|
|
17
|
+
function normalizeKitSource(src: string): string {
|
|
18
|
+
return src
|
|
19
|
+
.replace(/\(\s*0\s*,\s*(?:[\w$.]+\.)?(jsx(?:DEV)?|createElement)\)\s*\(/g, "$1(")
|
|
20
|
+
.replace(/(?:[\w$.]+\.)?(jsx(?:DEV)?|createElement)\(/g, "$1(");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `AlertTitle` → `Title`; `DialogDescription` → `Description`. */
|
|
24
|
+
export function slotLabelFromComponent(component: string): string {
|
|
25
|
+
const match = component.match(/^[A-Z][a-z]+(?=[A-Z])/);
|
|
26
|
+
const suffix = match ? component.slice(match[0].length) : component;
|
|
27
|
+
return suffix
|
|
28
|
+
.replace(/([A-Z])/g, " $1")
|
|
29
|
+
.replace(/^./, (c) => c.toUpperCase())
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Example copy for common compound slot names. */
|
|
34
|
+
export function slotDefaultFromComponent(component: string): string | undefined {
|
|
35
|
+
if (component.endsWith("Title")) return "Heads up";
|
|
36
|
+
if (component.endsWith("Description")) return "This is a short description.";
|
|
37
|
+
if (component.endsWith("Trigger") || component.includes("Trigger")) return "Open";
|
|
38
|
+
if (component.endsWith("Content")) return "Content";
|
|
39
|
+
if (component.endsWith("Label")) return "Label";
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RUNTIME_CHILDREN_SLOT_RE =
|
|
44
|
+
/(?:jsx(?:DEV)?|createElement)\(\s*([A-Za-z_$][\w$]*)\s*,\s*[^,]*,\s*([A-Za-z_$][\w$]*)\s*(?:\)|,)/g;
|
|
45
|
+
const RUNTIME_CHILDREN_PROP_RE =
|
|
46
|
+
/(?:jsx(?:DEV)?|createElement)\(\s*([A-Za-z_$][\w$]*)\s*,\s*\{[^}]*\bchildren:\s*([A-Za-z_$][\w$]*)/g;
|
|
47
|
+
const RUNTIME_ROOT_PROPS_RE =
|
|
48
|
+
/(?:jsx(?:DEV)?|createElement)\(\s*([A-Za-z_$][\w$]*)\s*,\s*\{([\s\S]*?)\}\s*(?:,|\))/g;
|
|
49
|
+
|
|
50
|
+
function collectJsxRuntimeSlots(src: string, out: KitJsxSlot[]): void {
|
|
51
|
+
const normalized = normalizeKitSource(src);
|
|
52
|
+
for (const match of normalized.matchAll(RUNTIME_CHILDREN_SLOT_RE)) {
|
|
53
|
+
out.push({ component: match[1]!, param: match[2]! });
|
|
54
|
+
}
|
|
55
|
+
for (const match of normalized.matchAll(RUNTIME_CHILDREN_PROP_RE)) {
|
|
56
|
+
out.push({ component: match[1]!, param: match[2]! });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function collectSourceJsxSlots(src: string, out: KitJsxSlot[]): void {
|
|
61
|
+
const re = /<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>\s*\{\s*([A-Za-z_$][\w$]*)\s*\}\s*<\/\1>/g;
|
|
62
|
+
for (const match of src.matchAll(re)) {
|
|
63
|
+
out.push({ component: match[1]!, param: match[2]! });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Map kit params to compound subcomponents (`AlertTitle`, etc.) from JSX. */
|
|
68
|
+
export function inferKitJsxSlots(fn: Function): KitJsxSlot[] {
|
|
69
|
+
const src = kitSource(fn);
|
|
70
|
+
const out: KitJsxSlot[] = [];
|
|
71
|
+
collectSourceJsxSlots(src, out);
|
|
72
|
+
collectJsxRuntimeSlots(src, out);
|
|
73
|
+
|
|
74
|
+
const seen = new Set<string>();
|
|
75
|
+
return out.filter((slot) => {
|
|
76
|
+
const id = `${slot.component}:${slot.param}`;
|
|
77
|
+
if (seen.has(id)) return false;
|
|
78
|
+
seen.add(id);
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseJsxPropsObject(raw: string | undefined): Array<{ prop: string; param: string }> {
|
|
84
|
+
if (!raw) return [];
|
|
85
|
+
const body = raw.trim();
|
|
86
|
+
const out: Array<{ prop: string; param: string }> = [];
|
|
87
|
+
|
|
88
|
+
if (/^[A-Za-z_$][\w$]*$/.test(body)) {
|
|
89
|
+
return [{ prop: body, param: body }];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const re = /\b([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\b/g;
|
|
93
|
+
for (const match of raw.matchAll(re)) {
|
|
94
|
+
const prop = match[1]!;
|
|
95
|
+
const param = match[2]!;
|
|
96
|
+
if (prop === "children" || prop === "className" || prop === "style" || prop === "asChild") {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
out.push({ prop, param });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ES shorthand `{ variant }` → `variant: variant`
|
|
103
|
+
const shorthandRe = /(?:^|[,{]\s*)([A-Za-z_$][\w$]*)\s*(?=[,}])/g;
|
|
104
|
+
for (const match of raw.matchAll(shorthandRe)) {
|
|
105
|
+
const prop = match[1]!;
|
|
106
|
+
if (
|
|
107
|
+
prop === "children" ||
|
|
108
|
+
prop === "className" ||
|
|
109
|
+
prop === "style" ||
|
|
110
|
+
prop === "asChild" ||
|
|
111
|
+
out.some((binding) => binding.prop === prop)
|
|
112
|
+
) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
out.push({ prop, param: prop });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function collectRootPropBindings(src: string, out: KitRootPropBinding[]): void {
|
|
122
|
+
const normalized = normalizeKitSource(src);
|
|
123
|
+
for (const match of normalized.matchAll(RUNTIME_ROOT_PROPS_RE)) {
|
|
124
|
+
for (const binding of parseJsxPropsObject(match[2])) {
|
|
125
|
+
out.push({ component: match[1]!, prop: binding.prop, param: binding.param });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sourceRoot = /<([A-Z][A-Za-z0-9]*)([^>]*)>/g;
|
|
130
|
+
for (const match of src.matchAll(sourceRoot)) {
|
|
131
|
+
const attrs = match[2] ?? "";
|
|
132
|
+
const attrRe = /\b([A-Za-z_$][\w$]*)\s*=\s*\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
133
|
+
for (const attr of attrs.matchAll(attrRe)) {
|
|
134
|
+
const prop = attr[1]!;
|
|
135
|
+
if (prop === "className" || prop === "style" || prop === "asChild") continue;
|
|
136
|
+
out.push({ component: match[1]!, prop, param: attr[2]! });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Root component prop bindings such as `<Alert variant={variant}>`. */
|
|
142
|
+
export function inferKitRootPropBindings(fn: Function): KitRootPropBinding[] {
|
|
143
|
+
const src = kitSource(fn);
|
|
144
|
+
const out: KitRootPropBinding[] = [];
|
|
145
|
+
collectRootPropBindings(src, out);
|
|
146
|
+
|
|
147
|
+
const seen = new Set<string>();
|
|
148
|
+
return out.filter((binding) => {
|
|
149
|
+
const id = `${binding.component}.${binding.prop}:${binding.param}`;
|
|
150
|
+
if (seen.has(id)) return false;
|
|
151
|
+
seen.add(id);
|
|
152
|
+
return true;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function primaryRootComponent(fn: Function): string | undefined {
|
|
157
|
+
const bindings = inferKitRootPropBindings(fn);
|
|
158
|
+
if (bindings.length > 0) return bindings[0]!.component;
|
|
159
|
+
const slots = inferKitJsxSlots(fn);
|
|
160
|
+
for (const slot of slots) {
|
|
161
|
+
const prefix = slot.component.match(/^[A-Z][a-z]+(?=[A-Z])/)?.[0];
|
|
162
|
+
if (prefix && slot.component.startsWith(prefix)) return prefix;
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { controlsFromKitParams, inferKitParams } from "./inferKitParams";
|
|
3
|
+
|
|
4
|
+
describe("inferKitParams", () => {
|
|
5
|
+
it("reads simple destructured keys", () => {
|
|
6
|
+
const kit = ({ title, description }: { title: string; description: string }) =>
|
|
7
|
+
`${title}-${description}`;
|
|
8
|
+
expect(inferKitParams(kit)).toEqual([{ key: "title" }, { key: "description" }]);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("reads inline defaults from destructuring", () => {
|
|
12
|
+
const kit = ({ placeholder = "Pick a stack" }: { placeholder?: string }) => placeholder;
|
|
13
|
+
expect(inferKitParams(kit)).toEqual([
|
|
14
|
+
{ key: "placeholder", defaultValue: "Pick a stack" },
|
|
15
|
+
]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("reads boolean defaults", () => {
|
|
19
|
+
const kit = ({ showEmail = false }: { showEmail?: boolean }) => showEmail;
|
|
20
|
+
expect(inferKitParams(kit)).toEqual([{ key: "showEmail", defaultValue: false }]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns empty for parameterless kits", () => {
|
|
24
|
+
const kit = () => "static";
|
|
25
|
+
expect(inferKitParams(kit)).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns empty for non-destructured params", () => {
|
|
29
|
+
const kit = (props: { title: string }) => props.title;
|
|
30
|
+
expect(inferKitParams(kit)).toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("builds control defaults from inferred params", () => {
|
|
34
|
+
expect(
|
|
35
|
+
controlsFromKitParams([
|
|
36
|
+
{ key: "title" },
|
|
37
|
+
{ key: "placeholder", defaultValue: "Pick one" },
|
|
38
|
+
]),
|
|
39
|
+
).toEqual({ title: "title", placeholder: "Pick one" });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { CompactPlaygroundControl } from "./expandPlaygroundControls";
|
|
2
|
+
|
|
3
|
+
export type InferredKitParam = {
|
|
4
|
+
key: string;
|
|
5
|
+
defaultValue?: CompactPlaygroundControl;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function splitTopLevelCommas(input: string): string[] {
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
let depth = 0;
|
|
11
|
+
let quote: "'" | '"' | "`" | null = null;
|
|
12
|
+
let start = 0;
|
|
13
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
14
|
+
const ch = input[i]!;
|
|
15
|
+
if (quote) {
|
|
16
|
+
if (ch === quote && input[i - 1] !== "\\") quote = null;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
20
|
+
quote = ch;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (ch === "{" || ch === "[" || ch === "(") depth += 1;
|
|
24
|
+
else if (ch === "}" || ch === "]" || ch === ")") depth -= 1;
|
|
25
|
+
else if (ch === "," && depth === 0) {
|
|
26
|
+
parts.push(input.slice(start, i));
|
|
27
|
+
start = i + 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
parts.push(input.slice(start));
|
|
31
|
+
return parts;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseLiteral(raw: string): CompactPlaygroundControl | undefined {
|
|
35
|
+
const value = raw.trim();
|
|
36
|
+
if (value === "true") return true;
|
|
37
|
+
if (value === "false") return false;
|
|
38
|
+
if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
|
|
39
|
+
const quoted =
|
|
40
|
+
(value.startsWith("'") && value.endsWith("'")) ||
|
|
41
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
42
|
+
(value.startsWith("`") && value.endsWith("`"));
|
|
43
|
+
if (quoted && value.length >= 2) return value.slice(1, -1);
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseDestructuredSegment(segment: string): InferredKitParam | null {
|
|
48
|
+
const trimmed = segment.trim();
|
|
49
|
+
if (!trimmed || trimmed.startsWith("...")) return null;
|
|
50
|
+
|
|
51
|
+
const assignIdx = findTopLevelChar(trimmed, "=");
|
|
52
|
+
if (assignIdx !== -1) {
|
|
53
|
+
const left = trimmed.slice(0, assignIdx).trim();
|
|
54
|
+
const defaultRaw = trimmed.slice(assignIdx + 1).trim();
|
|
55
|
+
const key = left.includes(":") ? left.split(":")[0]!.trim() : left;
|
|
56
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key)) return null;
|
|
57
|
+
const defaultValue = parseLiteral(defaultRaw);
|
|
58
|
+
return defaultValue !== undefined ? { key, defaultValue } : { key };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const colonIdx = findTopLevelChar(trimmed, ":");
|
|
62
|
+
if (colonIdx !== -1) {
|
|
63
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
64
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(key)) return null;
|
|
65
|
+
return { key };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(trimmed)) return null;
|
|
69
|
+
return { key: trimmed };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findTopLevelChar(input: string, target: string): number {
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let quote: "'" | '"' | "`" | null = null;
|
|
75
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
76
|
+
const ch = input[i]!;
|
|
77
|
+
if (quote) {
|
|
78
|
+
if (ch === quote && input[i - 1] !== "\\") quote = null;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
82
|
+
quote = ch;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === "{" || ch === "[" || ch === "(") depth += 1;
|
|
86
|
+
else if (ch === "}" || ch === "]" || ch === ")") depth -= 1;
|
|
87
|
+
else if (ch === target && depth === 0) return i;
|
|
88
|
+
}
|
|
89
|
+
return -1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function objectDestructuringSource(fn: Function): string | undefined {
|
|
93
|
+
const src = fn.toString().trim();
|
|
94
|
+
const match = src.match(/^(?:async\s*)?(?:function\s*)?\(\s*\{([\s\S]*?)\}\s*(?::[^)=]*)?\)/);
|
|
95
|
+
return match?.[1];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Read destructured parameter keys (and inline defaults) from a kit callback. */
|
|
99
|
+
export function inferKitParams(fn: Function): InferredKitParam[] {
|
|
100
|
+
const inner = objectDestructuringSource(fn);
|
|
101
|
+
if (!inner) return [];
|
|
102
|
+
return splitTopLevelCommas(inner)
|
|
103
|
+
.map(parseDestructuredSegment)
|
|
104
|
+
.filter((param): param is InferredKitParam => param !== null);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function controlsFromKitParams(params: InferredKitParam[]): Record<string, CompactPlaygroundControl> {
|
|
108
|
+
const out: Record<string, CompactPlaygroundControl> = {};
|
|
109
|
+
for (const param of params) {
|
|
110
|
+
out[param.key] = param.defaultValue ?? param.key;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Program, TypeChecker, SourceFile, Type } from "typescript";
|
|
2
|
+
import type { PlaygroundSpec } from "../types/report.js";
|
|
3
|
+
|
|
4
|
+
export type PropKind = "boolean" | "string" | "number";
|
|
5
|
+
|
|
6
|
+
export type CheckerProgram = {
|
|
7
|
+
program: Program;
|
|
8
|
+
checker: TypeChecker;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export declare function createCheckerProgram(projectRoot: string): CheckerProgram | null;
|
|
12
|
+
|
|
13
|
+
export declare function findComponentParamType(
|
|
14
|
+
checker: TypeChecker,
|
|
15
|
+
sf: SourceFile,
|
|
16
|
+
exportName: string,
|
|
17
|
+
): Type | undefined;
|
|
18
|
+
|
|
19
|
+
export declare function classifyPropType(checker: TypeChecker, type: Type): PropKind | null;
|
|
20
|
+
|
|
21
|
+
export declare function extractFiniteStringUnion(
|
|
22
|
+
checker: TypeChecker,
|
|
23
|
+
type: Type,
|
|
24
|
+
): string[] | null;
|
|
25
|
+
|
|
26
|
+
export declare function inferDeclaredPropKindsFromTs(
|
|
27
|
+
checker: TypeChecker,
|
|
28
|
+
program: Program,
|
|
29
|
+
projectRoot: string,
|
|
30
|
+
relPath: string,
|
|
31
|
+
exportName: string,
|
|
32
|
+
declaredProps: string[],
|
|
33
|
+
): Record<string, string>;
|
|
34
|
+
|
|
35
|
+
export declare function enrichPlaygroundSpecFromTs(
|
|
36
|
+
spec: PlaygroundSpec,
|
|
37
|
+
checker: TypeChecker,
|
|
38
|
+
program: Program,
|
|
39
|
+
projectRoot: string,
|
|
40
|
+
): PlaygroundSpec;
|
|
41
|
+
|
|
42
|
+
export declare function enrichPlaygroundsFromReport(
|
|
43
|
+
report: { playgrounds?: PlaygroundSpec[] },
|
|
44
|
+
checker: TypeChecker,
|
|
45
|
+
program: Program,
|
|
46
|
+
projectRoot: string,
|
|
47
|
+
): void;
|