|
| 1 | +import fs from "fs"; |
| 2 | +import path from "path"; |
| 3 | +import parser from "@babel/parser"; |
| 4 | +import _traverse from "@babel/traverse"; |
| 5 | +import _generate from "@babel/generator"; |
| 6 | +import { fileURLToPath } from "url"; |
| 7 | +import type { File } from "@babel/types"; |
| 8 | + |
| 9 | +const traverse = (_traverse as unknown as { default: typeof _traverse }).default ?? _traverse; |
| 10 | +const generate = (_generate as unknown as { default: typeof _generate }).default ?? _generate; |
| 11 | + |
| 12 | +const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 13 | +const componentsDir = path.resolve(__dirname, "../../../docs/src/pages/components/"); |
| 14 | +const outputDir = path.resolve(__dirname, "../../dist/metadata/examples/"); |
| 15 | + |
| 16 | +function getStoryFiles(): string[] { |
| 17 | + const storyFiles: string[] = []; |
| 18 | + |
| 19 | + function traverseDirectory(dir: string): void { |
| 20 | + const entries = fs.readdirSync(dir, { withFileTypes: true }); |
| 21 | + for (const entry of entries) { |
| 22 | + const fullPath = path.join(dir, entry.name); |
| 23 | + if (entry.isDirectory()) { |
| 24 | + traverseDirectory(fullPath); |
| 25 | + } else if (entry.isFile()) { |
| 26 | + if (entry.name.endsWith(".stories.tsx") || entry.name.endsWith(".stories.js")) { |
| 27 | + storyFiles.push(fullPath); |
| 28 | + } |
| 29 | + } |
| 30 | + } |
| 31 | + } |
| 32 | + |
| 33 | + traverseDirectory(componentsDir); |
| 34 | + return storyFiles; |
| 35 | +} |
| 36 | + |
| 37 | +export function generateCodeForOneLiner(ast: File, constName: string): string | null { |
| 38 | + let calleeCode: string | null = null; |
| 39 | + traverse(ast, { |
| 40 | + VariableDeclarator(nodePath) { |
| 41 | + if ((nodePath.node.id as { name?: string }).name === constName) { |
| 42 | + const init = nodePath.node.init as { callee?: { name?: string } } | null; |
| 43 | + if (init?.callee?.name !== "createComponentTemplate") { |
| 44 | + calleeCode = "const " + generate(nodePath.node).code; |
| 45 | + } else { |
| 46 | + calleeCode = ""; |
| 47 | + } |
| 48 | + } |
| 49 | + } |
| 50 | + }); |
| 51 | + return calleeCode; |
| 52 | +} |
| 53 | + |
| 54 | +function extractMarkdown(file: string): void { |
| 55 | + const componentName = path.basename(file).split(".")[0]; |
| 56 | + const outputFile = path.join(outputDir, componentName + ".md"); |
| 57 | + const fileContent = fs.readFileSync(file, "utf-8"); |
| 58 | + |
| 59 | + const ast = parser.parse(fileContent, { |
| 60 | + sourceType: "module", |
| 61 | + plugins: ["typescript", "jsx"] |
| 62 | + }); |
| 63 | + |
| 64 | + let markdown = "# Storybook Code Examples\n\n"; |
| 65 | + |
| 66 | + traverse(ast, { |
| 67 | + ExportNamedDeclaration(nodePath) { |
| 68 | + if (nodePath.node.declaration && nodePath.node.declaration.type === "VariableDeclaration") { |
| 69 | + nodePath.node.declaration.declarations.forEach(declarator => { |
| 70 | + const storyName = (declarator.id as { name?: string }).name ?? ""; |
| 71 | + if (declarator.init && declarator.init.type === "ObjectExpression") { |
| 72 | + let renderProp: { type: string; body?: unknown; callee?: { object?: { name?: string } } } | null = null; |
| 73 | + let nameProp: string | null = null; |
| 74 | + let codeBlock = ""; |
| 75 | + |
| 76 | + declarator.init.properties.forEach(prop => { |
| 77 | + if (prop.type !== "ObjectProperty") return; |
| 78 | + const key = prop.key as { name?: string }; |
| 79 | + const value = prop.value as { type: string; value?: string }; |
| 80 | + if (key.name === "render") { |
| 81 | + renderProp = value as typeof renderProp; |
| 82 | + } |
| 83 | + if (key.name === "name" && value.type === "StringLiteral") { |
| 84 | + nameProp = value.value ?? null; |
| 85 | + } |
| 86 | + }); |
| 87 | + |
| 88 | + if (renderProp) { |
| 89 | + const rp = renderProp as { |
| 90 | + type: string; |
| 91 | + body?: { type: string; body?: unknown[] }; |
| 92 | + callee?: { object?: { name?: string } }; |
| 93 | + }; |
| 94 | + if (rp.type === "ArrowFunctionExpression") { |
| 95 | + const body = rp.body as { type: string; body?: unknown[] }; |
| 96 | + if (body.type === "JSXFragment" || body.type === "MemberExpression" || body.type === "JSXElement") { |
| 97 | + codeBlock = generate(body as Parameters<typeof generate>[0]).code; |
| 98 | + } else if (body.type === "BlockStatement") { |
| 99 | + codeBlock = (body.body ?? []) |
| 100 | + .map(line => generate(line as Parameters<typeof generate>[0]).code) |
| 101 | + .join("\n"); |
| 102 | + } else { |
| 103 | + codeBlock = generate(body as Parameters<typeof generate>[0]).code; |
| 104 | + } |
| 105 | + } else if (rp.type === "CallExpression") { |
| 106 | + const calleeName = rp.callee?.object?.name ?? ""; |
| 107 | + codeBlock = generateCodeForOneLiner(ast, calleeName) ?? ""; |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + const displayName = nameProp ?? storyName; |
| 112 | + if (codeBlock.length > 0) { |
| 113 | + markdown += `## ${displayName}\n\n\`\`\`tsx\n${codeBlock.trim()}\n\`\`\`\n\n`; |
| 114 | + } |
| 115 | + } |
| 116 | + }); |
| 117 | + } |
| 118 | + } |
| 119 | + }); |
| 120 | + |
| 121 | + fs.writeFileSync(outputFile, markdown, "utf-8"); |
| 122 | +} |
| 123 | + |
| 124 | +export function run(): void { |
| 125 | + if (!fs.existsSync(componentsDir)) { |
| 126 | + console.error(`Components directory not found: ${componentsDir}`); |
| 127 | + process.exit(1); |
| 128 | + } |
| 129 | + |
| 130 | + fs.mkdirSync(outputDir, { recursive: true }); |
| 131 | + |
| 132 | + const files = getStoryFiles(); |
| 133 | + console.log(`Extracting examples from ${files.length} story files...`); |
| 134 | + files.forEach(file => extractMarkdown(file)); |
| 135 | + console.log("Examples extraction complete"); |
| 136 | +} |
| 137 | + |
| 138 | +const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); |
| 139 | +if (isMain) { |
| 140 | + run(); |
| 141 | +} |
0 commit comments