diff --git a/packages/mcp/scripts/__tests__/extract-accessibility.test.js b/packages/mcp/scripts/__tests__/extract-accessibility.test.js
new file mode 100644
index 0000000000..7cf65a16c5
--- /dev/null
+++ b/packages/mcp/scripts/__tests__/extract-accessibility.test.js
@@ -0,0 +1,162 @@
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import { describe, it, expect } from "vitest";
+import { cleanUpAccessibilityContent, run } from "../extract-accessibility.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const outputDir = path.join(__dirname, "../../dist/generated/accessibility/");
+
+describe("cleanUpAccessibilityContent", () => {
+ describe("UsageGuidelines extraction", () => {
+ it("should extract numbered guidelines from quoted strings", () => {
+ const content = ``;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toBe(
+ "1. Use labels for all interactive elements\n2. Ensure color contrast meets WCAG AA"
+ );
+ });
+
+ it("should extract guidelines wrapped in React fragments", () => {
+ const content = `First guideline text>,
+ <>Second guideline text>
+ ]} />`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toBe("1. First guideline text\n2. Second guideline text");
+ });
+
+ it("should convert tags to backticks within guidelines", () => {
+ const content = `Use the aria-label prop for accessibility>
+ ]} />`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toContain("`aria-label`");
+ expect(result).not.toContain("");
+ expect(result).not.toContain("");
+ });
+
+ it("should handle mixed quoted strings and React fragments", () => {
+ const content = `Fragment with code inside>
+ ]} />`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toContain("1. Simple string guideline");
+ expect(result).toContain("2. Fragment with `code` inside");
+ });
+
+ it("should number guidelines sequentially", () => {
+ const content = ``;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toBe("1. First\n2. Second\n3. Third");
+ });
+
+ it("should skip whitespace-only React fragment guidelines", () => {
+ const content = `Valid guideline>,
+ <> >,
+ <>Another valid guideline>
+ ]} />`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toBe("1. Valid guideline\n2. Another valid guideline");
+ });
+ });
+
+ describe("fallback cleanup", () => {
+ it("should remove UsageGuidelines JSX components", () => {
+ const content = `Some text before\n\nSome text after`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).not.toContain("UsageGuidelines");
+ expect(result).toContain("Some text before");
+ expect(result).toContain("Some text after");
+ });
+
+ it("should replace tags with backticks in fallback mode", () => {
+ const content = `Use the role attribute for elements.`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toBe("Use the `role` attribute for elements.");
+ });
+
+ it("should remove React fragment tags in fallback mode", () => {
+ const content = `<>Some content inside fragments>`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).toBe("Some content inside fragments");
+ });
+
+ it("should remove lines with only brackets or punctuation", () => {
+ const content = `Valid content\n [\n ]\nMore valid content`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).not.toMatch(/^\s*[\[\]]\s*$/m);
+ expect(result).toContain("Valid content");
+ expect(result).toContain("More valid content");
+ });
+
+ it("should reduce multiple empty lines to double newlines", () => {
+ const content = `First paragraph\n\n\n\nSecond paragraph`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).not.toContain("\n\n\n");
+ expect(result).toContain("First paragraph\n\nSecond paragraph");
+ });
+
+ it("should return fallback message for empty content", () => {
+ const result = cleanUpAccessibilityContent("");
+
+ expect(result).toBe("No accessibility guidelines found in expected format.");
+ });
+
+ it("should return fallback message for whitespace-only content", () => {
+ const result = cleanUpAccessibilityContent(" \n \n ");
+
+ expect(result).toBe("No accessibility guidelines found in expected format.");
+ });
+
+ it("should remove guidelines= attribute syntax in fallback mode", () => {
+ const content = `Some preamble\nguidelines={something}\nAfter text`;
+
+ const result = cleanUpAccessibilityContent(content);
+
+ expect(result).not.toContain("guidelines=");
+ expect(result).toContain("Some preamble");
+ expect(result).toContain("After text");
+ });
+ });
+});
+
+describe("run - integration", () => {
+ it("should generate accessibility markdown files from MDX files", () => {
+ run();
+
+ const outputFiles = fs.readdirSync(outputDir).filter(f => f.endsWith(".md"));
+ expect(outputFiles.length).toBeGreaterThan(0);
+ });
+});
diff --git a/packages/mcp/scripts/__tests__/extract-code-samples.test.js b/packages/mcp/scripts/__tests__/extract-code-samples.test.js
new file mode 100644
index 0000000000..54b1f00a56
--- /dev/null
+++ b/packages/mcp/scripts/__tests__/extract-code-samples.test.js
@@ -0,0 +1,113 @@
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import { describe, it, expect } from "vitest";
+import parser from "@babel/parser";
+import { generateCodeForOneLiner, run } from "../extract-code-samples.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const outputDir = path.join(__dirname, "../../dist/generated/");
+
+function parseCode(code) {
+ return parser.parse(code, {
+ sourceType: "module",
+ plugins: ["typescript", "jsx"]
+ });
+}
+
+describe("generateCodeForOneLiner", () => {
+ it("should return generated code for a regular variable declaration", () => {
+ const code = `const myComponent = () => Hello
;`;
+ const ast = parseCode(code);
+
+ const result = generateCodeForOneLiner(ast, "myComponent");
+
+ expect(result).not.toBeNull();
+ expect(result).toContain("const");
+ expect(result).toContain("myComponent");
+ });
+
+ it("should return empty string for a createComponentTemplate call", () => {
+ const code = `const myTemplate = createComponentTemplate(SomeComponent);`;
+ const ast = parseCode(code);
+
+ const result = generateCodeForOneLiner(ast, "myTemplate");
+
+ expect(result).toBe("");
+ });
+
+ it("should return null when the variable name is not found", () => {
+ const code = `const other = 42;`;
+ const ast = parseCode(code);
+
+ const result = generateCodeForOneLiner(ast, "nonExistent");
+
+ expect(result).toBeNull();
+ });
+
+ it("should handle multiple variable declarations and find the correct one", () => {
+ const code = `
+ const first = createComponentTemplate(A);
+ const second = () => Test;
+ const third = 42;
+ `;
+ const ast = parseCode(code);
+
+ expect(generateCodeForOneLiner(ast, "first")).toBe("");
+ expect(generateCodeForOneLiner(ast, "second")).toContain("second");
+ expect(generateCodeForOneLiner(ast, "third")).toContain("third");
+ });
+
+ it("should prepend 'const ' to the generated code", () => {
+ const code = `const myVar = "hello";`;
+ const ast = parseCode(code);
+
+ const result = generateCodeForOneLiner(ast, "myVar");
+
+ expect(result).toMatch(/^const /);
+ });
+});
+
+describe("generateCodeForOneLiner - JSX handling", () => {
+ it("should generate code for a variable with JSX value", () => {
+ const code = `const myElement = Hello
;`;
+ const ast = parseCode(code);
+
+ const result = generateCodeForOneLiner(ast, "myElement");
+
+ expect(result).toContain("myElement");
+ expect(result).toContain("div");
+ expect(result).toContain("wrapper");
+ });
+
+ it("should generate code for an arrow function returning JSX", () => {
+ const code = `const MyComponent = () => ;`;
+ const ast = parseCode(code);
+
+ const result = generateCodeForOneLiner(ast, "MyComponent");
+
+ expect(result).not.toBeNull();
+ expect(result).toContain("MyComponent");
+ expect(result).toContain("Button");
+ });
+
+ it("should handle a variable with a nested function call (not createComponentTemplate)", () => {
+ const code = `const myVar = someOtherFunction(Arg1, Arg2);`;
+ const ast = parseCode(code);
+
+ const result = generateCodeForOneLiner(ast, "myVar");
+
+ expect(result).not.toBeNull();
+ expect(result).toContain("const");
+ expect(result).toContain("someOtherFunction");
+ });
+});
+
+describe("run - integration", () => {
+ it("should generate markdown files from story files", () => {
+ run();
+
+ const outputFiles = fs.readdirSync(outputDir).filter(f => f.endsWith(".md"));
+ expect(outputFiles.length).toBeGreaterThan(0);
+ });
+});
diff --git a/packages/mcp/scripts/extract-accessibility.js b/packages/mcp/scripts/extract-accessibility.js
index 42ed5b73f5..aeafe0e8a5 100644
--- a/packages/mcp/scripts/extract-accessibility.js
+++ b/packages/mcp/scripts/extract-accessibility.js
@@ -7,11 +7,7 @@ const __dirname = path.dirname(__filename);
const componentsDir = path.join(__dirname, "../../docs/src/pages/components/");
const outputDir = path.join(__dirname, "../dist/generated/accessibility/");
-if (!fs.existsSync(outputDir)) {
- fs.mkdirSync(outputDir, { recursive: true });
-}
-
-function getMdxFiles() {
+export function getMdxFiles() {
const mdxFiles = [];
function traverseDirectory(dir) {
@@ -36,7 +32,7 @@ function getMdxFiles() {
return mdxFiles;
}
-function extractAccessibilityFromMdx(file) {
+export function extractAccessibilityFromMdx(file) {
const inputFile = path.join(__dirname, file);
const inputFileComponentName = path.basename(inputFile).split(".")[0];
const outputFile = path.resolve(__dirname, outputDir, inputFileComponentName + ".md");
@@ -91,7 +87,7 @@ function extractAccessibilityFromMdx(file) {
console.log(`Accessibility extracted for ${inputFileComponentName} at ${outputFile}`);
}
-function cleanUpAccessibilityContent(content) {
+export function cleanUpAccessibilityContent(content) {
// Extract guidelines from UsageGuidelines component
const guidelinesMatch = content.match(/guidelines=\{(\[[\s\S]*?\])\}/);
@@ -143,7 +139,11 @@ function cleanUpAccessibilityContent(content) {
return cleaned || "No accessibility guidelines found in expected format.";
}
-function run() {
+export function run() {
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
console.log("🔍 Extracting accessibility content from MDX files...");
const mdxFiles = getMdxFiles();
@@ -162,4 +162,6 @@ function run() {
console.log("✅ Accessibility extraction completed!");
}
-run();
+if (process.argv[1] === __filename) {
+ run();
+}
diff --git a/packages/mcp/scripts/extract-code-samples.js b/packages/mcp/scripts/extract-code-samples.js
index 2c656ef8af..3238eab00b 100644
--- a/packages/mcp/scripts/extract-code-samples.js
+++ b/packages/mcp/scripts/extract-code-samples.js
@@ -1,19 +1,19 @@
import fs from "fs";
import path from "path";
import parser from "@babel/parser";
-import traverse from "@babel/traverse";
-import generate from "@babel/generator";
+import _traverse from "@babel/traverse";
+import _generate from "@babel/generator";
+
+const traverse = _traverse.default ?? _traverse;
+const generate = _generate.default ?? _generate;
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const componentsDir = path.join(__dirname, "../../docs/src/pages/components/");
const outputDir = path.join(__dirname, "../dist/generated/");
-if (!fs.existsSync(outputDir)) {
- fs.mkdirSync(outputDir);
-}
-function getStoryFiles() {
+export function getStoryFiles() {
const storyFiles = [];
function traverseDirectory(dir) {
@@ -38,15 +38,13 @@ function getStoryFiles() {
return storyFiles;
}
-const files = getStoryFiles();
-
-function generateCodeForOneLiner(ast, constName) {
+export function generateCodeForOneLiner(ast, constName) {
let calleeCode = null;
- traverse.default(ast, {
+ traverse(ast, {
VariableDeclarator(path) {
if (path.node.id.name === constName) {
if (path.node.init?.callee?.name !== "createComponentTemplate") {
- calleeCode = "const " + generate.default(path.node).code;
+ calleeCode = "const " + generate(path.node).code;
} else {
return (calleeCode = "");
}
@@ -56,7 +54,7 @@ function generateCodeForOneLiner(ast, constName) {
return calleeCode;
}
-function extractMarkdown(file) {
+export function extractMarkdown(file) {
const inputFile = path.join(__dirname, file);
const inputFileComponentName = path.basename(inputFile).split(".")[0];
const outputFile = path.resolve(__dirname, outputDir, inputFileComponentName + ".md");
@@ -74,17 +72,17 @@ function extractMarkdown(file) {
let markdown = "# Storybook Code Examples\n\n";
let accordionTemplateCode = "";
- traverse.default(ast, {
+ traverse(ast, {
VariableDeclarator(path) {
// console.log(path.node);
if (path.node.id.name === "accordionTemplate" && path.node.init.type === "ArrowFunctionExpression") {
- accordionTemplateCode = generate.default(path.node.init).code;
+ accordionTemplateCode = generate(path.node.init).code;
}
}
});
// Traverse the AST to find all exported story objects
- traverse.default(ast, {
+ traverse(ast, {
ExportNamedDeclaration(path) {
if (path.node.declaration && path.node.declaration.type === "VariableDeclaration") {
path.node.declaration.declarations.forEach(declarator => {
@@ -113,12 +111,12 @@ function extractMarkdown(file) {
renderProp.body.type == "MemberExpression" ||
renderProp.body.type == "JSXElement"
) {
- codeBlock = generate.default(renderProp.body).code;
+ codeBlock = generate(renderProp.body).code;
} else if (renderProp.body.type == "BlockStatement") {
- codeBlock = renderProp.body.body.map(line => generate.default(line).code).join("\n");
+ codeBlock = renderProp.body.body.map(line => generate(line).code).join("\n");
} else {
console.log(`${nameProp || storyName} is a ${renderProp.body.type}`);
- codeBlock = generate.default(renderProp.body).code;
+ codeBlock = generate(renderProp.body).code;
}
} else if (renderProp.type == "CallExpression") {
// console.log(renderProp.callee.object.name);
@@ -142,10 +140,16 @@ function extractMarkdown(file) {
console.log(`Code samples generated for ${inputFileComponentName} at ${outputFile}`);
}
-function run() {
+export function run() {
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir);
+ }
+ const files = getStoryFiles();
files.forEach(file => {
extractMarkdown(file);
});
}
-run();
+if (process.argv[1] === __filename) {
+ run();
+}
diff --git a/packages/mcp/vitest.config.ts b/packages/mcp/vitest.config.ts
new file mode 100644
index 0000000000..105034bbab
--- /dev/null
+++ b/packages/mcp/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ include: ["**/__tests__/**/*.test.js"],
+ clearMocks: true
+ }
+});