MCP compose tool
This commit is contained in:
parent
6545b53310
commit
0e5033414b
53
packages/mcp/src/tools/compose.ts
Normal file
53
packages/mcp/src/tools/compose.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type { ComponentRegistry } from "../registry.js";
|
||||
|
||||
interface ComposeInput { components: string[]; }
|
||||
interface ComposeResult { jsx: string; imports: string[]; warnings: string[]; }
|
||||
|
||||
function toKebab(str: string): string {
|
||||
return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
}
|
||||
|
||||
function toPascal(kebab: string): string {
|
||||
return kebab.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
||||
}
|
||||
|
||||
function generateComponentJsx(name: string, parts: readonly string[], requiredParts: readonly string[], indent: number): string {
|
||||
const pad = " ".repeat(indent);
|
||||
const lines: string[] = [];
|
||||
if (parts.includes("Root")) {
|
||||
lines.push(`${pad}<${name}>`);
|
||||
for (const part of requiredParts) {
|
||||
if (part === "Root") continue;
|
||||
lines.push(`${pad} <${name}.${part}>...</${name}.${part}>`);
|
||||
}
|
||||
lines.push(`${pad}</${name}>`);
|
||||
} else {
|
||||
lines.push(`${pad}<${name} />`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Converts a pettyui/ export path to an ES import statement. */
|
||||
function toImportLine(imp: string): string {
|
||||
const name = toPascal(imp.replace("pettyui/", ""));
|
||||
return `import { ${name} } from "${imp}";`;
|
||||
}
|
||||
|
||||
/** Generates composed JSX from multiple components. */
|
||||
export function handleCompose(registry: ComponentRegistry, input: ComposeInput): ComposeResult {
|
||||
const imports: string[] = [];
|
||||
const jsxParts: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
for (const name of input.components) {
|
||||
const comp = registry.getComponent(name);
|
||||
if (!comp) { warnings.push(`Component "${name}" not found`); continue; }
|
||||
imports.push(comp.exportPath);
|
||||
jsxParts.push(generateComponentJsx(comp.meta.name, comp.meta.parts, comp.meta.requiredParts, 1));
|
||||
}
|
||||
|
||||
const importLines = imports.map(toImportLine);
|
||||
const jsx = [...importLines, "", "function Component() {", " return (", " <>", jsxParts.join("\n"), " </>", " );", "}"].join("\n");
|
||||
|
||||
return { jsx, imports, warnings };
|
||||
}
|
||||
33
packages/mcp/tests/tools/compose.test.ts
Normal file
33
packages/mcp/tests/tools/compose.test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { handleCompose } from "../../src/tools/compose.js";
|
||||
import { ComponentRegistry } from "../../src/registry.js";
|
||||
|
||||
describe("pettyui.compose", () => {
|
||||
const registry = new ComponentRegistry();
|
||||
|
||||
it("generates JSX for specified components — Dialog + Button both present", () => {
|
||||
const result = handleCompose(registry, { components: ["Dialog", "Button"] });
|
||||
expect(result.jsx).toContain("Dialog");
|
||||
expect(result.jsx).toContain("Button");
|
||||
expect(result.imports).toContain("pettyui/dialog");
|
||||
expect(result.imports).toContain("pettyui/button");
|
||||
});
|
||||
|
||||
it("generates imports for all — Form + TextField + Button → 3 imports", () => {
|
||||
const result = handleCompose(registry, { components: ["Form", "TextField", "Button"] });
|
||||
expect(result.imports).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("uses required parts — Dialog → jsx contains Dialog.Content and Dialog.Title", () => {
|
||||
const result = handleCompose(registry, { components: ["Dialog"] });
|
||||
expect(result.jsx).toContain("Dialog.Content");
|
||||
expect(result.jsx).toContain("Dialog.Title");
|
||||
});
|
||||
|
||||
it("skips unknown with warning — FakeComponent gets warning, Dialog still in jsx", () => {
|
||||
const result = handleCompose(registry, { components: ["Dialog", "FakeComponent"] });
|
||||
expect(result.warnings).toContain('Component "FakeComponent" not found');
|
||||
expect(result.jsx).toContain("Dialog");
|
||||
expect(result.imports).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user