UNPKG

storybook-addon-jsdoc-to-mdx

Version:

Storybook addon that automatically generates MDX documentation from JSDoc comments in your TypeScript and JavaScript files. Supports HTML tags in comments, complex TypeScript types, and integrates seamlessly with Storybook 7.x and 8.x.

132 lines (131 loc) 5.53 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.removeCommentsFromCode = removeCommentsFromCode; exports.findBasePath = findBasePath; exports.getPathName = getPathName; exports.extractMethodFromCode = extractMethodFromCode; exports.formatJsDocComment = formatJsDocComment; const path_1 = __importDefault(require("path")); const typescript_1 = __importDefault(require("typescript")); const comment_parser_1 = require("comment-parser"); function removeCommentsFromCode(code) { const printer = typescript_1.default.createPrinter({ removeComments: true }); const sourceFile = typescript_1.default.createSourceFile("temp.ts", code, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS); return printer.printFile(sourceFile); } function findBasePath(filePath, folderPaths) { for (const folderPath of folderPaths) { if (filePath.startsWith(folderPath)) { return folderPath; } } return ""; } function getPathName(filePath, baseDir) { const relativePath = path_1.default.relative(baseDir, filePath); const dirName = path_1.default.dirname(relativePath); const extenstion = path_1.default.extname(filePath); const baseName = path_1.default.basename(relativePath, extenstion); if (baseName === "index") { return dirName.replace(/\\/g, "/"); } return path_1.default.join(dirName, baseName).replace(/\\/g, "/"); } function extractMethodFromCode(code, methodName) { const sourceFile = typescript_1.default.createSourceFile("temp.ts", code, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS); let methodNode; function visit(node) { if (typescript_1.default.isMethodDeclaration(node) && node.name && node.name.getText() === methodName) { methodNode = node; return; } typescript_1.default.forEachChild(node, visit); } visit(sourceFile); if (!methodNode) { return code; } const start = methodNode.getStart(sourceFile); const end = methodNode.end; return code.slice(start, end).trim(); } // Escape HTML tags to prevent MDX parsing errors function escapeHtmlTags(str) { return str .replace(/[-`]/g, '\\$&') // \- and \` .replace(/[{}]/g, '\\$&') // \{ and \} .replace(/</g, '&lt;') // < .replace(/>/g, '&gt;'); // > } function formatJsDocComment(raw) { var _a; const trimmedRaw = raw.trim(); // Return plain strings unchanged (non JSDoc comments) if (!trimmedRaw.startsWith('/**')) { return trimmedRaw; } // Parse the raw JSDoc comment const { description = '', tags = [] } = ((_a = (0, comment_parser_1.parse)(trimmedRaw)) === null || _a === void 0 ? void 0 : _a[0]) || {}; // Group tags by their `tag` field const grouped = tags.reduce((acc, t) => { var _a; (acc[t.tag] = (_a = acc[t.tag]) !== null && _a !== void 0 ? _a : []).push(t); return acc; }, {}); // Render the JSDoc description as a Markdown block-quote const quotedDescription = description .trim() .split('\n') .map(line => `> ${escapeHtmlTags(line)}`) .join('\n'); let mdx = quotedDescription + '\n\n'; // Parameters if (grouped.param) { const paramsBlock = grouped.param .map(t => `- \`${t.name}\` ${t.type ? `*${escapeHtmlTags(t.type)}*` : ''}${escapeHtmlTags(t.description.trim().replace(/^-/g, '').trim())}`) .join('\n'); mdx += `#### Parameter:\n\n${paramsBlock}\n`; } // Returns const returnsGroup = grouped.returns || grouped.return; if (returnsGroup) { const { type = '', description: retDesc = '' } = returnsGroup[0]; mdx += `#### Returns:\n${type ? `\`${escapeHtmlTags(type)}\`` : ''} ${escapeHtmlTags(retDesc)}\n`; } // Example if (grouped.example && grouped.example.length > 0) { mdx += `#### Example:\n`; grouped.example.forEach(e => { const { description: exDesc = '', name: exName = '', source = [] } = e; // Prefer raw source tokens because they keep the original line breaks const rawLines = source .filter(line => typeof line.source === 'string') .slice(1, -1) .map(line => line.source.replace(/^\s*\*\s?/, '')); // strip leading "* " // Drop the first line that still contains "@example" const codeLines = rawLines.length > 1 ? rawLines // use tokens if present : exDesc.split('\n'); // fallback to description const exampleCode = codeLines.join('\n'); mdx += `\`\`\`ts\n${exampleCode}\n\`\`\`\n`; }); } // Fallback for other tags Object.entries(grouped).forEach(([tagName, tagList]) => { if (['param', 'return', 'returns', 'example'].includes(tagName)) return; const heading = tagName.charAt(0).toUpperCase() + tagName.slice(1); const lines = tagList .map(t => t.name ? `- \`${t.name}\`${t.type ? ` *${t.type}*` : ''}${escapeHtmlTags(t.description)}` : escapeHtmlTags(t.description)) .join('\n'); mdx += `#### ${heading}:\n${lines}\n`; }); return mdx.trim(); }