| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /** |
| * ============================================================================ |
| * PHILOSOPHY: STORIES ARE THE SINGLE SOURCE OF TRUTH |
| * ============================================================================ |
| * |
| * When something doesn't render correctly in the docs, FIX THE STORY FIRST. |
| * Do NOT add special cases or workarounds to this generator. |
| * |
| * This generator should be as lightweight as possible - it extracts data from |
| * stories and passes it through to MDX. All configuration belongs in stories: |
| * |
| * - Use `export default { title: '...' }` (inline export, not variable) |
| * - Name stories `Interactive${ComponentName}` for docs generation |
| * - Define `args` and `argTypes` at the story level (not meta level) |
| * - Use `parameters.docs.gallery` for variant grids |
| * - Use `parameters.docs.sampleChildren` for components needing children |
| * - Use `parameters.docs.liveExample` for custom code examples |
| * - Use `parameters.docs.staticProps` for complex props |
| * |
| * If a story doesn't work with this generator, fix the story to match the |
| * expected patterns rather than adding complexity here. |
| * ============================================================================ |
| */ |
| |
| /** |
| * This script scans for ALL Storybook stories and generates MDX documentation |
| * pages for the "Superset Components" section of the developer portal. |
| * |
| * Supports multiple source directories with different import paths and categories. |
| * |
| * Usage: node scripts/generate-superset-components.mjs |
| */ |
| |
| import fs from 'fs'; |
| import path from 'path'; |
| import { fileURLToPath } from 'url'; |
| |
| const __filename = fileURLToPath(import.meta.url); |
| const __dirname = path.dirname(__filename); |
| const ROOT_DIR = path.resolve(__dirname, '../..'); |
| const DOCS_DIR = path.resolve(__dirname, '..'); |
| const OUTPUT_DIR = path.join(DOCS_DIR, 'developer_portal/components'); |
| const FRONTEND_DIR = path.join(ROOT_DIR, 'superset-frontend'); |
| |
| // Source configurations with import paths and categories |
| const SOURCES = [ |
| { |
| name: 'UI Core Components', |
| path: 'packages/superset-ui-core/src/components', |
| importPrefix: '@superset/components', |
| docImportPrefix: '@superset-ui/core/components', |
| category: 'ui', |
| enabled: true, |
| // Components that require complex function props or aren't exported properly |
| skipComponents: new Set([ |
| // Complex function props (require callbacks, async data, or render props) |
| 'AsyncSelect', 'ConfirmStatusChange', 'CronPicker', 'LabeledErrorBoundInput', |
| 'AsyncAceEditor', 'AsyncEsmComponent', 'TimezoneSelector', |
| // Not exported from @superset/components index or have export mismatches |
| 'ActionCell', 'BooleanCell', 'ButtonCell', 'NullCell', 'NumericCell', 'TimeCell', |
| 'CertifiedBadgeWithTooltip', 'CodeSyntaxHighlighter', 'DynamicTooltip', |
| 'PopoverDropdown', 'PopoverSection', 'WarningIconWithTooltip', 'RefreshLabel', |
| // Components with complex nested props (JSX children, overlay, items arrays) |
| 'Dropdown', 'DropdownButton', |
| ]), |
| }, |
| { |
| name: 'App Components', |
| path: 'src/components', |
| importPrefix: 'src/components', |
| docImportPrefix: 'src/components', |
| category: 'app', |
| enabled: false, // Requires app context (Redux, routing, etc.) |
| skipComponents: new Set([]), |
| }, |
| { |
| name: 'Dashboard Components', |
| path: 'src/dashboard/components', |
| importPrefix: 'src/dashboard/components', |
| docImportPrefix: 'src/dashboard/components', |
| category: 'dashboard', |
| enabled: false, // Requires app context |
| skipComponents: new Set([]), |
| }, |
| { |
| name: 'Explore Components', |
| path: 'src/explore/components', |
| importPrefix: 'src/explore/components', |
| docImportPrefix: 'src/explore/components', |
| category: 'explore', |
| enabled: false, // Requires app context |
| skipComponents: new Set([]), |
| }, |
| { |
| name: 'Feature Components', |
| path: 'src/features', |
| importPrefix: 'src/features', |
| docImportPrefix: 'src/features', |
| category: 'features', |
| enabled: false, // Requires app context |
| skipComponents: new Set([]), |
| }, |
| { |
| name: 'Filter Components', |
| path: 'src/filters/components', |
| importPrefix: 'src/filters/components', |
| docImportPrefix: 'src/filters/components', |
| category: 'filters', |
| enabled: false, // Requires app context |
| skipComponents: new Set([]), |
| }, |
| { |
| name: 'Chart Plugins', |
| path: 'packages/superset-ui-demo/storybook/stories/plugins', |
| importPrefix: '@superset-ui/demo', |
| docImportPrefix: '@superset-ui/demo', |
| category: 'chart-plugins', |
| enabled: false, // Requires chart infrastructure |
| skipComponents: new Set([]), |
| }, |
| { |
| name: 'Core Packages', |
| path: 'packages/superset-ui-demo/storybook/stories/superset-ui-chart', |
| importPrefix: '@superset-ui/core', |
| docImportPrefix: '@superset-ui/core', |
| category: 'core-packages', |
| enabled: false, // Requires specific setup |
| skipComponents: new Set([]), |
| }, |
| ]; |
| |
| // Category mapping from story title prefixes to output directories |
| const CATEGORY_MAP = { |
| 'Components/': 'ui', |
| 'Design System/': 'design-system', |
| 'Chart Plugins/': 'chart-plugins', |
| 'Legacy Chart Plugins/': 'legacy-charts', |
| 'Core Packages/': 'core-packages', |
| 'Others/': 'utilities', |
| 'Extension Components/': 'extension', // Skip - handled by other script |
| 'Superset App/': 'app', |
| }; |
| |
| // Documentation-only stories to skip (not actual components) |
| const SKIP_STORIES = [ |
| 'Introduction', // Design System intro page |
| 'Overview', // Category overview pages |
| 'Examples', // Example collections |
| 'DesignSystem', // Meta design system page |
| 'MetadataBarOverview', // Overview page |
| 'TableOverview', // Overview page |
| 'Filter Plugins', // Collection story, not a component |
| ]; |
| |
| |
| /** |
| * Recursively find all story files in a directory |
| */ |
| function walkDir(dir, files = []) { |
| if (!fs.existsSync(dir)) return files; |
| const entries = fs.readdirSync(dir, { withFileTypes: true }); |
| for (const entry of entries) { |
| const fullPath = path.join(dir, entry.name); |
| if (entry.isDirectory()) { |
| walkDir(fullPath, files); |
| } else if (entry.name.endsWith('.stories.tsx') || entry.name.endsWith('.stories.ts')) { |
| files.push(fullPath); |
| } |
| } |
| return files; |
| } |
| |
| /** |
| * Find all story files from enabled sources |
| */ |
| function findEnabledStoryFiles() { |
| const files = []; |
| for (const source of SOURCES.filter(s => s.enabled)) { |
| const dir = path.join(FRONTEND_DIR, source.path); |
| const sourceFiles = walkDir(dir, []); |
| // Attach source config to each file |
| for (const file of sourceFiles) { |
| files.push({ file, source }); |
| } |
| } |
| return files; |
| } |
| |
| /** |
| * Find all story files from disabled sources (for tracking) |
| */ |
| function findDisabledStoryFiles() { |
| const files = []; |
| for (const source of SOURCES.filter(s => !s.enabled)) { |
| const dir = path.join(FRONTEND_DIR, source.path); |
| walkDir(dir, files); |
| } |
| return files; |
| } |
| |
| /** |
| * Parse a story file and extract metadata |
| */ |
| function parseStoryFile(filePath, sourceConfig) { |
| const content = fs.readFileSync(filePath, 'utf-8'); |
| |
| // Extract title from story meta (in export default block, not from data objects) |
| // Look for title in the export default section, which typically starts with "export default {" |
| const metaMatch = content.match(/export\s+default\s*\{[\s\S]*?title:\s*['"]([^'"]+)['"]/); |
| const title = metaMatch ? metaMatch[1] : null; |
| |
| if (!title) return null; |
| |
| // Extract component name (last part of title path) |
| const titleParts = title.split('/'); |
| const componentName = titleParts.pop(); |
| |
| // Skip documentation-only stories |
| if (SKIP_STORIES.includes(componentName)) { |
| return null; |
| } |
| |
| // Skip components in the source's skip list |
| if (sourceConfig.skipComponents.has(componentName)) { |
| return null; |
| } |
| |
| // Determine category - use source's default category unless title has a specific prefix |
| let category = sourceConfig.category; |
| for (const [prefix, cat] of Object.entries(CATEGORY_MAP)) { |
| if (title.startsWith(prefix)) { |
| category = cat; |
| break; |
| } |
| } |
| |
| // Extract description from parameters |
| let description = ''; |
| const descBlockMatch = content.match( |
| /description:\s*{\s*component:\s*([\s\S]*?)\s*},?\s*}/ |
| ); |
| if (descBlockMatch) { |
| const descBlock = descBlockMatch[1]; |
| const stringParts = []; |
| const stringMatches = descBlock.matchAll(/['"]([^'"]*)['"]/g); |
| for (const match of stringMatches) { |
| stringParts.push(match[1]); |
| } |
| description = stringParts.join('').trim(); |
| } |
| |
| // Extract story exports |
| const storyExports = []; |
| const exportMatches = content.matchAll(/export\s+(?:const|function)\s+(\w+)/g); |
| for (const match of exportMatches) { |
| if (match[1] !== 'default') { |
| storyExports.push(match[1]); |
| } |
| } |
| |
| // Extract component import path from the story file |
| // Look for: import ComponentName from './path' (default export) |
| // or: import { ComponentName } from './path' (named export) |
| let componentImportPath = null; |
| let isDefaultExport = true; |
| |
| // Try to find default import matching the component name |
| // Handles: import Component from 'path' |
| // and: import Component, { OtherExport } from 'path' |
| const defaultImportMatch = content.match( |
| new RegExp(`import\\s+${componentName}(?:\\s*,\\s*{[^}]*})?\\s+from\\s+['"]([^'"]+)['"]`) |
| ); |
| if (defaultImportMatch) { |
| componentImportPath = defaultImportMatch[1]; |
| isDefaultExport = true; |
| } else { |
| // Try named import |
| const namedImportMatch = content.match( |
| new RegExp(`import\\s*{[^}]*\\b${componentName}\\b[^}]*}\\s*from\\s+['"]([^'"]+)['"]`) |
| ); |
| if (namedImportMatch) { |
| componentImportPath = namedImportMatch[1]; |
| isDefaultExport = false; |
| } |
| } |
| |
| // Calculate full import path if we found a relative import |
| // For UI core components with aliases, keep using the alias |
| let resolvedImportPath = sourceConfig.importPrefix; |
| const useAlias = sourceConfig.importPrefix.startsWith('@superset/'); |
| |
| if (componentImportPath && componentImportPath.startsWith('.') && !useAlias) { |
| const storyDir = path.dirname(filePath); |
| const resolvedPath = path.resolve(storyDir, componentImportPath); |
| // Get path relative to frontend root, then convert to import path |
| const frontendRelative = path.relative(FRONTEND_DIR, resolvedPath); |
| resolvedImportPath = frontendRelative.replace(/\\/g, '/'); |
| } else if (!componentImportPath && !useAlias) { |
| // Fallback: assume component is in same dir as story, named same as component |
| const storyDir = path.dirname(filePath); |
| const possibleComponentPath = path.join(storyDir, componentName); |
| const frontendRelative = path.relative(FRONTEND_DIR, possibleComponentPath); |
| resolvedImportPath = frontendRelative.replace(/\\/g, '/'); |
| } |
| |
| return { |
| filePath, |
| title, |
| titleParts, |
| componentName, |
| category, |
| description, |
| storyExports, |
| relativePath: path.relative(ROOT_DIR, filePath), |
| sourceConfig, |
| resolvedImportPath, |
| isDefaultExport, |
| }; |
| } |
| |
| /** |
| * Parse args content and extract key-value pairs |
| * Handles strings with apostrophes correctly |
| */ |
| function parseArgsContent(argsContent, args) { |
| // Split into lines and process each line for simple key-value pairs |
| const lines = argsContent.split('\n'); |
| |
| for (let i = 0; i < lines.length; i++) { |
| const trimmed = lines[i].trim(); |
| if (!trimmed || trimmed.startsWith('//')) continue; |
| |
| // Match: key: value pattern at start of line |
| const propMatch = trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*):\s*(.+?)[\s,]*$/); |
| // Also match key with value on the next line (e.g., prettier wrapping long strings) |
| const keyOnlyMatch = !propMatch && trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*):$/); |
| if (!propMatch && !keyOnlyMatch) continue; |
| |
| let key, valueStr; |
| if (propMatch) { |
| key = propMatch[1]; |
| valueStr = propMatch[2]; |
| } else { |
| // Value is on the next line |
| key = keyOnlyMatch[1]; |
| const nextLine = i + 1 < lines.length ? lines[i + 1].trim().replace(/,\s*$/, '') : ''; |
| if (!nextLine) continue; |
| valueStr = nextLine; |
| i++; // Skip the next line since we consumed it |
| } |
| |
| // Parse the value |
| // Double-quoted string (handles apostrophes inside) |
| const doubleQuoteMatch = valueStr.match(/^"([^"]*)"$/); |
| if (doubleQuoteMatch) { |
| args[key] = doubleQuoteMatch[1]; |
| continue; |
| } |
| |
| // Single-quoted string |
| const singleQuoteMatch = valueStr.match(/^'([^']*)'$/); |
| if (singleQuoteMatch) { |
| args[key] = singleQuoteMatch[1]; |
| continue; |
| } |
| |
| // Template literal |
| const templateMatch = valueStr.match(/^`([^`]*)`$/); |
| if (templateMatch) { |
| args[key] = templateMatch[1].replace(/\s+/g, ' ').trim(); |
| continue; |
| } |
| |
| // Boolean |
| if (valueStr === 'true' || valueStr === 'true,') { |
| args[key] = true; |
| continue; |
| } |
| if (valueStr === 'false' || valueStr === 'false,') { |
| args[key] = false; |
| continue; |
| } |
| |
| // Number (including decimals and negative) |
| const numMatch = valueStr.match(/^(-?\d+\.?\d*),?$/); |
| if (numMatch) { |
| args[key] = Number(numMatch[1]); |
| continue; |
| } |
| |
| // Skip complex values (objects, arrays, function calls, expressions) |
| } |
| } |
| |
| /** |
| * Extract variable arrays from file content (for options references) |
| */ |
| function extractVariableArrays(content) { |
| const variableArrays = {}; |
| |
| // Pattern 1: const varName = ['a', 'b', 'c']; |
| // Also handles: export const varName: Type[] = ['a', 'b', 'c']; |
| const varMatches = content.matchAll(/(?:export\s+)?(?:const|let)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?::\s*[^=]+)?\s*=\s*\[([^\]]+)\]/g); |
| for (const varMatch of varMatches) { |
| const varName = varMatch[1]; |
| const arrayContent = varMatch[2]; |
| const values = []; |
| const valMatches = arrayContent.matchAll(/['"]([^'"]+)['"]/g); |
| for (const val of valMatches) { |
| values.push(val[1]); |
| } |
| if (values.length > 0) { |
| variableArrays[varName] = values; |
| } |
| } |
| |
| // Pattern 2: const VAR = { options: [...] } - for SIZES.options, COLORS.options patterns |
| const objWithOptionsMatches = content.matchAll(/(?:const|let)\s+([A-Z][A-Z_0-9]*)\s*=\s*\{[^}]*options:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/g); |
| for (const match of objWithOptionsMatches) { |
| const objName = match[1]; |
| const optionsVarName = match[2]; |
| // Link the object's options to the underlying array |
| if (variableArrays[optionsVarName]) { |
| variableArrays[objName] = variableArrays[optionsVarName]; |
| } |
| } |
| |
| return variableArrays; |
| } |
| |
| /** |
| * Extract a string value from content, handling quotes properly |
| */ |
| function extractStringValue(content, startIndex) { |
| const remaining = content.slice(startIndex).trim(); |
| |
| // Single-quoted string |
| if (remaining.startsWith("'")) { |
| let i = 1; |
| while (i < remaining.length) { |
| if (remaining[i] === "'" && remaining[i - 1] !== '\\') { |
| return remaining.slice(1, i); |
| } |
| i++; |
| } |
| } |
| |
| // Double-quoted string |
| if (remaining.startsWith('"')) { |
| let i = 1; |
| while (i < remaining.length) { |
| if (remaining[i] === '"' && remaining[i - 1] !== '\\') { |
| return remaining.slice(1, i); |
| } |
| i++; |
| } |
| } |
| |
| // Template literal |
| if (remaining.startsWith('`')) { |
| let i = 1; |
| while (i < remaining.length) { |
| if (remaining[i] === '`' && remaining[i - 1] !== '\\') { |
| return remaining.slice(1, i).replace(/\s+/g, ' ').trim(); |
| } |
| i++; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Parse argTypes content and populate the argTypes object |
| */ |
| function parseArgTypes(argTypesContent, argTypes, fullContent) { |
| const variableArrays = extractVariableArrays(fullContent); |
| |
| // Match argType definitions - find each property block |
| // Use balanced brace extraction for each property |
| const propPattern = /([a-zA-Z_$][a-zA-Z0-9_$]*):\s*\{/g; |
| let propMatch; |
| |
| while ((propMatch = propPattern.exec(argTypesContent)) !== null) { |
| const propName = propMatch[1]; |
| const propStartIndex = propMatch.index + propMatch[0].length - 1; |
| const propConfig = extractBalancedBraces(argTypesContent, propStartIndex); |
| |
| if (!propConfig) continue; |
| |
| // Initialize argTypes entry if not exists |
| if (!argTypes[propName]) { |
| argTypes[propName] = {}; |
| } |
| |
| // Extract description - find the position and extract properly |
| const descIndex = propConfig.indexOf('description:'); |
| if (descIndex !== -1) { |
| const descValue = extractStringValue(propConfig, descIndex + 'description:'.length); |
| if (descValue) { |
| argTypes[propName].description = descValue; |
| } |
| } |
| |
| // Check for inline options array |
| const optionsMatch = propConfig.match(/options:\s*\[([^\]]+)\]/); |
| if (optionsMatch) { |
| const optionsStr = optionsMatch[1]; |
| const options = []; |
| const optionMatches = optionsStr.matchAll(/['"]([^'"]+)['"]/g); |
| for (const opt of optionMatches) { |
| options.push(opt[1]); |
| } |
| if (options.length > 0) { |
| argTypes[propName].type = 'select'; |
| argTypes[propName].options = options; |
| } |
| } else { |
| // Check for variable reference: options: variableName or options: VAR.options |
| const varRefMatch = propConfig.match(/options:\s*([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)?)/); |
| if (varRefMatch) { |
| const varRef = varRefMatch[1]; |
| // Handle VAR.options pattern |
| const dotMatch = varRef.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\.options$/); |
| if (dotMatch && variableArrays[dotMatch[1]]) { |
| argTypes[propName].type = 'select'; |
| argTypes[propName].options = variableArrays[dotMatch[1]]; |
| } else if (variableArrays[varRef]) { |
| argTypes[propName].type = 'select'; |
| argTypes[propName].options = variableArrays[varRef]; |
| } |
| } else { |
| // Check for ES6 shorthand: options, (same as options: options) |
| const shorthandMatch = propConfig.match(/(?:^|[,\s])options(?:[,\s]|$)/); |
| if (shorthandMatch && variableArrays['options']) { |
| argTypes[propName].type = 'select'; |
| argTypes[propName].options = variableArrays['options']; |
| } |
| } |
| } |
| |
| // Check for control type (radio, select, boolean, etc.) |
| // Supports both: control: 'boolean' (shorthand) and control: { type: 'boolean' } (object) |
| const controlShorthandMatch = propConfig.match(/control:\s*['"]([^'"]+)['"]/); |
| const controlObjectMatch = propConfig.match(/control:\s*\{[^}]*type:\s*['"]([^'"]+)['"]/); |
| if (controlShorthandMatch) { |
| argTypes[propName].type = controlShorthandMatch[1]; |
| } else if (controlObjectMatch) { |
| argTypes[propName].type = controlObjectMatch[1]; |
| } |
| |
| // Clear options for non-select/radio types (the shorthand "options" detection |
| // can false-positive when the word "options" appears in description text) |
| const finalType = argTypes[propName].type; |
| if (finalType && !['select', 'radio', 'inline-radio'].includes(finalType)) { |
| delete argTypes[propName].options; |
| } |
| } |
| } |
| |
| /** |
| * Helper to find balanced braces content |
| */ |
| function extractBalancedBraces(content, startIndex) { |
| let depth = 0; |
| let start = -1; |
| for (let i = startIndex; i < content.length; i++) { |
| if (content[i] === '{') { |
| if (depth === 0) start = i + 1; |
| depth++; |
| } else if (content[i] === '}') { |
| depth--; |
| if (depth === 0) { |
| return content.slice(start, i); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Helper to find balanced brackets content (for arrays) |
| */ |
| function extractBalancedBrackets(content, startIndex) { |
| let depth = 0; |
| let start = -1; |
| for (let i = startIndex; i < content.length; i++) { |
| if (content[i] === '[') { |
| if (depth === 0) start = i + 1; |
| depth++; |
| } else if (content[i] === ']') { |
| depth--; |
| if (depth === 0) { |
| return content.slice(start, i); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Convert camelCase prop name to human-readable label |
| * Handles acronyms properly: imgURL -> "Image URL", coverLeft -> "Cover Left" |
| */ |
| function propNameToLabel(name) { |
| return name |
| // Insert space before uppercase letters that follow lowercase (camelCase boundary) |
| .replace(/([a-z])([A-Z])/g, '$1 $2') |
| // Handle common acronyms - keep them together |
| .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') |
| // Capitalize first letter |
| .replace(/^./, s => s.toUpperCase()) |
| // Fix common acronyms display |
| .replace(/\bUrl\b/g, 'URL') |
| .replace(/\bImg\b/g, 'Image') |
| .replace(/\bId\b/g, 'ID'); |
| } |
| |
| /** |
| * Convert JS object literal syntax to JSON |
| * Handles: single quotes, unquoted keys, trailing commas |
| */ |
| function jsToJson(jsStr) { |
| try { |
| // Remove comments |
| let str = jsStr.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, ''); |
| |
| // Replace single quotes with double quotes (but not inside already double-quoted strings) |
| str = str.replace(/'/g, '"'); |
| |
| // Add quotes around unquoted keys: { foo: -> { "foo": |
| str = str.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)(\s*:)/g, '$1"$2"$3'); |
| |
| // Remove trailing commas before } or ] |
| str = str.replace(/,(\s*[}\]])/g, '$1'); |
| |
| return JSON.parse(str); |
| } catch { |
| return null; |
| } |
| } |
| |
| /** |
| * Extract docs config from story parameters |
| * Looks for: StoryName.parameters = { docs: { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample } } |
| * Uses generic JSON parsing for inline data |
| */ |
| function extractDocsConfig(content, storyNames) { |
| // Extract variable arrays for gallery config (sizes, styles) |
| const variableArrays = extractVariableArrays(content); |
| |
| let sampleChildren = null; |
| let sampleChildrenStyle = null; |
| let gallery = null; |
| let staticProps = null; |
| let liveExample = null; |
| let examples = null; |
| let renderComponent = null; |
| let triggerProp = null; |
| let onHideProp = null; |
| |
| for (const storyName of storyNames) { |
| // Look for parameters block |
| const parametersPattern = new RegExp(`${storyName}\\.parameters\\s*=\\s*\\{`, 's'); |
| const parametersMatch = content.match(parametersPattern); |
| |
| if (parametersMatch) { |
| const parametersContent = extractBalancedBraces(content, parametersMatch.index + parametersMatch[0].length - 1); |
| if (parametersContent) { |
| // Extract sampleChildren - inline array using generic JSON parser |
| const sampleChildrenArrayMatch = parametersContent.match(/sampleChildren:\s*\[/); |
| if (sampleChildrenArrayMatch) { |
| const arrayStartIndex = sampleChildrenArrayMatch.index + sampleChildrenArrayMatch[0].length - 1; |
| const arrayContent = extractBalancedBrackets(parametersContent, arrayStartIndex); |
| if (arrayContent) { |
| const parsed = jsToJson('[' + arrayContent + ']'); |
| if (parsed && parsed.length > 0) { |
| sampleChildren = parsed; |
| } |
| } |
| } |
| |
| // Extract sampleChildrenStyle - inline object using generic JSON parser |
| const sampleChildrenStyleMatch = parametersContent.match(/sampleChildrenStyle:\s*\{/); |
| if (sampleChildrenStyleMatch) { |
| const styleContent = extractBalancedBraces(parametersContent, sampleChildrenStyleMatch.index + sampleChildrenStyleMatch[0].length - 1); |
| if (styleContent) { |
| const parsed = jsToJson('{' + styleContent + '}'); |
| if (parsed) { |
| sampleChildrenStyle = parsed; |
| } |
| } |
| } |
| |
| // Extract staticProps - generic JSON-like object extraction |
| const staticPropsMatch = parametersContent.match(/staticProps:\s*\{/); |
| if (staticPropsMatch) { |
| const staticPropsContent = extractBalancedBraces(parametersContent, staticPropsMatch.index + staticPropsMatch[0].length - 1); |
| if (staticPropsContent) { |
| // Try to parse as JSON (handles inline data) |
| const parsed = jsToJson('{' + staticPropsContent + '}'); |
| if (parsed) { |
| staticProps = parsed; |
| } |
| } |
| } |
| |
| // Extract gallery config |
| const galleryMatch = parametersContent.match(/gallery:\s*\{/); |
| if (galleryMatch) { |
| const galleryContent = extractBalancedBraces(parametersContent, galleryMatch.index + galleryMatch[0].length - 1); |
| if (galleryContent) { |
| gallery = {}; |
| |
| // Extract component name |
| const compMatch = galleryContent.match(/component:\s*['"]([^'"]+)['"]/); |
| if (compMatch) gallery.component = compMatch[1]; |
| |
| // Extract sizes - variable reference |
| const sizesVarMatch = galleryContent.match(/sizes:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/); |
| if (sizesVarMatch && variableArrays[sizesVarMatch[1]]) { |
| gallery.sizes = variableArrays[sizesVarMatch[1]]; |
| } |
| |
| // Extract styles - variable reference |
| const stylesVarMatch = galleryContent.match(/styles:\s*([a-zA-Z_$][a-zA-Z0-9_$]*)/); |
| if (stylesVarMatch && variableArrays[stylesVarMatch[1]]) { |
| gallery.styles = variableArrays[stylesVarMatch[1]]; |
| } |
| |
| // Extract sizeProp |
| const sizePropMatch = galleryContent.match(/sizeProp:\s*['"]([^'"]+)['"]/); |
| if (sizePropMatch) gallery.sizeProp = sizePropMatch[1]; |
| |
| // Extract styleProp |
| const stylePropMatch = galleryContent.match(/styleProp:\s*['"]([^'"]+)['"]/); |
| if (stylePropMatch) gallery.styleProp = stylePropMatch[1]; |
| } |
| } |
| |
| // Extract liveExample - template literal for custom live code block |
| const liveExampleMatch = parametersContent.match(/liveExample:\s*`/); |
| if (liveExampleMatch) { |
| // Find the closing backtick |
| const startIndex = liveExampleMatch.index + liveExampleMatch[0].length; |
| let endIndex = startIndex; |
| while (endIndex < parametersContent.length && parametersContent[endIndex] !== '`') { |
| // Handle escaped backticks |
| if (parametersContent[endIndex] === '\\' && parametersContent[endIndex + 1] === '`') { |
| endIndex += 2; |
| } else { |
| endIndex++; |
| } |
| } |
| if (endIndex < parametersContent.length) { |
| // Unescape template literal escapes (source text has \` and \$ for literal backticks/dollars) |
| liveExample = parametersContent.slice(startIndex, endIndex).replace(/\\`/g, '`').replace(/\\\$/g, '$'); |
| } |
| } |
| |
| // Extract renderComponent - allows overriding which component to render |
| // Useful when the title-derived component (e.g., 'Icons') is a namespace, not a component |
| const renderComponentMatch = parametersContent.match(/renderComponent:\s*['"]([^'"]+)['"]/); |
| if (renderComponentMatch) { |
| renderComponent = renderComponentMatch[1]; |
| } |
| |
| // Extract triggerProp/onHideProp - for components like Modal that need a trigger button |
| const triggerPropMatch = parametersContent.match(/triggerProp:\s*['"]([^'"]+)['"]/); |
| if (triggerPropMatch) { |
| triggerProp = triggerPropMatch[1]; |
| } |
| const onHidePropMatch = parametersContent.match(/onHideProp:\s*['"]([^'"]+)['"]/); |
| if (onHidePropMatch) { |
| onHideProp = onHidePropMatch[1]; |
| } |
| |
| // Extract examples array - for multiple code examples |
| // Format: examples: [{ title: 'Title', code: `...` }, ...] |
| const examplesMatch = parametersContent.match(/examples:\s*\[/); |
| if (examplesMatch) { |
| const examplesStartIndex = examplesMatch.index + examplesMatch[0].length - 1; |
| const examplesArrayContent = extractBalancedBrackets(parametersContent, examplesStartIndex); |
| if (examplesArrayContent) { |
| examples = []; |
| // Find each example object { title: '...', code: `...` } |
| const exampleObjPattern = /\{\s*title:\s*['"]([^'"]+)['"]\s*,\s*code:\s*`/g; |
| let exampleMatch; |
| while ((exampleMatch = exampleObjPattern.exec(examplesArrayContent)) !== null) { |
| const title = exampleMatch[1]; |
| const codeStartIndex = exampleMatch.index + exampleMatch[0].length; |
| // Find closing backtick for code |
| let codeEndIndex = codeStartIndex; |
| while (codeEndIndex < examplesArrayContent.length && examplesArrayContent[codeEndIndex] !== '`') { |
| if (examplesArrayContent[codeEndIndex] === '\\' && examplesArrayContent[codeEndIndex + 1] === '`') { |
| codeEndIndex += 2; |
| } else { |
| codeEndIndex++; |
| } |
| } |
| // Unescape template literal escapes (source text has \` and \$ for literal backticks/dollars) |
| const code = examplesArrayContent.slice(codeStartIndex, codeEndIndex).replace(/\\`/g, '`').replace(/\\\$/g, '$'); |
| examples.push({ title, code }); |
| } |
| } |
| } |
| } |
| } |
| |
| if (sampleChildren || gallery || staticProps || liveExample || examples || renderComponent || triggerProp) break; |
| } |
| |
| return { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp }; |
| } |
| |
| /** |
| * Extract args and controls from story content |
| */ |
| function extractArgsAndControls(content, componentName) { |
| const args = {}; |
| const argTypes = {}; |
| |
| // First, extract argTypes from the default export meta (shared across all stories) |
| // Pattern: export default { argTypes: {...} } |
| const defaultExportMatch = content.match(/export\s+default\s*\{/); |
| if (defaultExportMatch) { |
| const metaContent = extractBalancedBraces(content, defaultExportMatch.index + defaultExportMatch[0].length - 1); |
| if (metaContent) { |
| const metaArgTypesMatch = metaContent.match(/\bargTypes:\s*\{/); |
| if (metaArgTypesMatch) { |
| const metaArgTypesContent = extractBalancedBraces(metaContent, metaArgTypesMatch.index + metaArgTypesMatch[0].length - 1); |
| if (metaArgTypesContent) { |
| parseArgTypes(metaArgTypesContent, argTypes, content); |
| } |
| } |
| } |
| } |
| |
| // Then, try to find the Interactive story block (CSF 3.0 or CSF 2.0) |
| // Support multiple naming conventions: |
| // - InteractiveComponentName (CSF 2.0 convention) |
| // - ComponentNameStory (CSF 3.0 convention) |
| // - ComponentName (fallback) |
| const storyNames = [`Interactive${componentName}`, `${componentName}Story`, componentName]; |
| |
| // Extract docs config (sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample) from parameters.docs |
| const { sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractDocsConfig(content, storyNames); |
| |
| for (const storyName of storyNames) { |
| // Try CSF 3.0 format: export const StoryName: StoryObj = { args: {...}, argTypes: {...} } |
| const csf3Pattern = new RegExp(`export\\s+const\\s+${storyName}[^=]*=[^{]*\\{`, 's'); |
| const csf3Match = content.match(csf3Pattern); |
| |
| if (csf3Match) { |
| const storyStartIndex = csf3Match.index + csf3Match[0].length - 1; |
| const storyContent = extractBalancedBraces(content, storyStartIndex); |
| |
| if (storyContent) { |
| // Extract args from story content |
| const argsMatch = storyContent.match(/\bargs:\s*\{/); |
| if (argsMatch) { |
| const argsContent = extractBalancedBraces(storyContent, argsMatch.index + argsMatch[0].length - 1); |
| if (argsContent) { |
| parseArgsContent(argsContent, args); |
| } |
| } |
| |
| // Extract argTypes from story content |
| const argTypesMatch = storyContent.match(/\bargTypes:\s*\{/); |
| if (argTypesMatch) { |
| const argTypesContent = extractBalancedBraces(storyContent, argTypesMatch.index + argTypesMatch[0].length - 1); |
| if (argTypesContent) { |
| parseArgTypes(argTypesContent, argTypes, content); |
| } |
| } |
| |
| if (Object.keys(args).length > 0 || Object.keys(argTypes).length > 0) { |
| break; // Found a matching story |
| } |
| } |
| } |
| |
| // Try CSF 2.0 format: StoryName.args = {...} |
| const csf2ArgsPattern = new RegExp(`${storyName}\\.args\\s*=\\s*\\{`, 's'); |
| const csf2ArgsMatch = content.match(csf2ArgsPattern); |
| if (csf2ArgsMatch) { |
| const argsContent = extractBalancedBraces(content, csf2ArgsMatch.index + csf2ArgsMatch[0].length - 1); |
| if (argsContent) { |
| parseArgsContent(argsContent, args); |
| } |
| } |
| |
| // Try CSF 2.0 argTypes: StoryName.argTypes = {...} |
| const csf2ArgTypesPattern = new RegExp(`${storyName}\\.argTypes\\s*=\\s*\\{`, 's'); |
| const csf2ArgTypesMatch = content.match(csf2ArgTypesPattern); |
| if (csf2ArgTypesMatch) { |
| const argTypesContent = extractBalancedBraces(content, csf2ArgTypesMatch.index + csf2ArgTypesMatch[0].length - 1); |
| if (argTypesContent) { |
| parseArgTypes(argTypesContent, argTypes, content); |
| } |
| } |
| |
| if (Object.keys(args).length > 0 || Object.keys(argTypes).length > 0) { |
| break; // Found a matching story |
| } |
| } |
| |
| // Generate controls from args first, then add any argTypes-only props |
| const controls = []; |
| const processedProps = new Set(); |
| |
| // First pass: props that have default values in args |
| for (const [key, value] of Object.entries(args)) { |
| processedProps.add(key); |
| const label = propNameToLabel(key); |
| const argType = argTypes[key] || {}; |
| |
| if (argType.type) { |
| // Use argTypes override (select, radio with options) |
| controls.push({ |
| name: key, |
| label, |
| type: argType.type, |
| options: argType.options, |
| description: argType.description |
| }); |
| } else if (typeof value === 'boolean') { |
| controls.push({ name: key, label, type: 'boolean', description: argType.description }); |
| } else if (typeof value === 'string') { |
| controls.push({ name: key, label, type: 'text', description: argType.description }); |
| } else if (typeof value === 'number') { |
| controls.push({ name: key, label, type: 'number', description: argType.description }); |
| } |
| } |
| |
| // Second pass: props defined only in argTypes (no explicit value in args) |
| // Add controls for these, but don't set default values on the component |
| // (setting defaults like open: false or status: 'error' breaks component behavior) |
| for (const [key, argType] of Object.entries(argTypes)) { |
| if (processedProps.has(key)) continue; |
| if (!argType.type) continue; // Skip if no control type defined |
| |
| const label = propNameToLabel(key); |
| |
| // Don't add to args - let the component use its own defaults |
| |
| controls.push({ |
| name: key, |
| label, |
| type: argType.type, |
| options: argType.options, |
| description: argType.description |
| }); |
| } |
| |
| return { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp }; |
| } |
| |
| /** |
| * Generate MDX content for a component |
| */ |
| function generateMDX(component, storyContent) { |
| const { componentName, description, relativePath, category, sourceConfig, resolvedImportPath, isDefaultExport } = component; |
| |
| const { args, argTypes, controls, sampleChildren, sampleChildrenStyle, gallery, staticProps, liveExample, examples, renderComponent, triggerProp, onHideProp } = extractArgsAndControls(storyContent, componentName); |
| |
| // Merge staticProps into args for complex values (arrays, objects) that can't be parsed from inline args |
| const mergedArgs = { ...args, ...staticProps }; |
| |
| // Format JSON: unquote property names but keep double quotes for string values |
| // This avoids issues with single quotes in strings breaking MDX parsing |
| const controlsJson = JSON.stringify(controls, null, 2) |
| .replace(/"(\w+)":/g, '$1:'); |
| |
| const propsJson = JSON.stringify(mergedArgs, null, 2) |
| .replace(/"(\w+)":/g, '$1:'); |
| |
| // Format sampleChildren if present (from story's parameters.docs.sampleChildren) |
| const sampleChildrenJson = sampleChildren |
| ? JSON.stringify(sampleChildren) |
| : null; |
| |
| // Format sampleChildrenStyle if present (from story's parameters.docs.sampleChildrenStyle) |
| const sampleChildrenStyleJson = sampleChildrenStyle |
| ? JSON.stringify(sampleChildrenStyle).replace(/"(\w+)":/g, '$1:') |
| : null; |
| |
| // Format gallery config if present |
| const hasGallery = gallery && gallery.sizes && gallery.styles; |
| |
| // Extract children for proper JSX rendering |
| const childrenValue = mergedArgs.children; |
| |
| const liveExampleProps = Object.entries(mergedArgs) |
| .filter(([key]) => key !== 'children') |
| .map(([key, value]) => { |
| if (typeof value === 'string') return `${key}="${value}"`; |
| if (typeof value === 'boolean') return value ? key : null; |
| return `${key}={${JSON.stringify(value)}}`; |
| }) |
| .filter(Boolean) |
| .join('\n '); |
| |
| // Generate props table with descriptions from argTypes |
| const propsTable = Object.entries(mergedArgs).map(([key, value]) => { |
| const type = typeof value === 'boolean' ? 'boolean' : typeof value === 'string' ? 'string' : typeof value === 'number' ? 'number' : 'any'; |
| const desc = argTypes[key]?.description || '-'; |
| return `| \`${key}\` | \`${type}\` | \`${JSON.stringify(value)}\` | ${desc} |`; |
| }).join('\n'); |
| |
| // Calculate relative import path based on category depth |
| const importDepth = category.includes('/') ? 4 : 3; |
| const wrapperImportPrefix = '../'.repeat(importDepth); |
| |
| // Use resolved import path if available, otherwise fall back to source config |
| const componentImportPath = resolvedImportPath || sourceConfig.importPrefix; |
| |
| // Determine component description based on source |
| const defaultDesc = sourceConfig.category === 'ui' |
| ? `The ${componentName} component from Superset's UI library.` |
| : `The ${componentName} component from Superset.`; |
| |
| return `--- |
| title: ${componentName} |
| sidebar_label: ${componentName} |
| --- |
| |
| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| --> |
| |
| import { StoryWithControls${hasGallery ? ', ComponentGallery' : ''} } from '${wrapperImportPrefix}src/components/StorybookWrapper'; |
| |
| # ${componentName} |
| |
| ${description || defaultDesc} |
| ${hasGallery ? ` |
| ## All Variants |
| |
| <ComponentGallery |
| component="${gallery.component || componentName}" |
| sizes={${JSON.stringify(gallery.sizes)}} |
| styles={${JSON.stringify(gallery.styles)}} |
| sizeProp="${gallery.sizeProp || 'size'}" |
| styleProp="${gallery.styleProp || 'variant'}" |
| /> |
| ` : ''} |
| ## Live Example |
| |
| <StoryWithControls |
| component="${componentName}"${renderComponent ? ` |
| renderComponent="${renderComponent}"` : ''} |
| props={${propsJson}} |
| controls={${controlsJson}}${sampleChildrenJson ? ` |
| sampleChildren={${sampleChildrenJson}}` : ''}${sampleChildrenStyleJson ? ` |
| sampleChildrenStyle={${sampleChildrenStyleJson}}` : ''}${triggerProp ? ` |
| triggerProp="${triggerProp}"` : ''}${onHideProp ? ` |
| onHideProp="${onHideProp}"` : ''} |
| /> |
| |
| ## Try It |
| |
| Edit the code below to experiment with the component: |
| |
| \`\`\`tsx live |
| ${liveExample || `function Demo() { |
| return ( |
| <${componentName} |
| ${liveExampleProps || '// Add props here'} |
| ${childrenValue ? `> |
| ${childrenValue} |
| </${componentName}>` : '/>'} |
| ); |
| }`} |
| \`\`\` |
| ${examples && examples.length > 0 ? examples.map(ex => ` |
| ## ${ex.title} |
| |
| \`\`\`tsx live |
| ${ex.code} |
| \`\`\` |
| `).join('') : ''} |
| ${Object.keys(args).length > 0 ? `## Props |
| |
| | Prop | Type | Default | Description | |
| |------|------|---------|-------------| |
| ${propsTable}` : ''} |
| |
| ## Import |
| |
| \`\`\`tsx |
| ${isDefaultExport ? `import ${componentName} from '${componentImportPath}';` : `import { ${componentName} } from '${componentImportPath}';`} |
| \`\`\` |
| |
| --- |
| |
| :::tip[Improve this page] |
| This documentation is auto-generated from the component's Storybook story. |
| Help improve it by [editing the story file](https://github.com/apache/superset/edit/master/${relativePath}). |
| ::: |
| `; |
| } |
| |
| /** |
| * Category display names for sidebar |
| */ |
| const CATEGORY_LABELS = { |
| ui: { title: 'Core Components', sidebarLabel: 'Core Components', description: 'Buttons, inputs, modals, selects, and other fundamental UI elements.' }, |
| 'design-system': { title: 'Layout Components', sidebarLabel: 'Layout Components', description: 'Grid, Layout, Table, Flex, Space, and container components for page structure.' }, |
| }; |
| |
| /** |
| * Generate category index page |
| */ |
| function generateCategoryIndex(category, components) { |
| const labels = CATEGORY_LABELS[category] || { |
| title: category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' '), |
| sidebarLabel: category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' '), |
| }; |
| const componentList = components |
| .sort((a, b) => a.componentName.localeCompare(b.componentName)) |
| .map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()})`) |
| .join('\n'); |
| |
| return `--- |
| title: ${labels.title} |
| sidebar_label: ${labels.sidebarLabel} |
| sidebar_position: 1 |
| --- |
| |
| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| --> |
| |
| # ${labels.title} |
| |
| ${components.length} components available in this category. |
| |
| ## Components |
| |
| ${componentList} |
| `; |
| } |
| |
| /** |
| * Generate main overview page |
| */ |
| function generateOverviewIndex(categories) { |
| const categoryList = Object.entries(categories) |
| .filter(([, components]) => components.length > 0) |
| .map(([cat, components]) => { |
| const labels = CATEGORY_LABELS[cat] || { |
| title: cat.charAt(0).toUpperCase() + cat.slice(1).replace(/-/g, ' '), |
| }; |
| const desc = labels.description ? ` ${labels.description}` : ''; |
| return `### [${labels.title}](./${cat}/)\n${components.length} components —${desc}\n`; |
| }) |
| .join('\n'); |
| |
| const totalComponents = Object.values(categories).reduce((sum, c) => sum + c.length, 0); |
| |
| return `--- |
| title: UI Components Overview |
| sidebar_label: Overview |
| sidebar_position: 0 |
| --- |
| |
| <!-- |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| --> |
| |
| # Superset Design System |
| |
| A design system is a complete set of standards intended to manage design at scale using reusable components and patterns. |
| |
| The Superset Design System uses [Atomic Design](https://bradfrost.com/blog/post/atomic-web-design/) principles with adapted terminology: |
| |
| | Atomic Design | Atoms | Molecules | Organisms | Templates | Pages / Screens | |
| |---|:---:|:---:|:---:|:---:|:---:| |
| | **Superset Design** | Foundations | Components | Patterns | Templates | Features | |
| |
| <img src="/img/atomic-design.png" alt="Atoms = Foundations, Molecules = Components, Organisms = Patterns, Templates = Templates, Pages / Screens = Features" style={{maxWidth: '100%'}} /> |
| |
| --- |
| |
| ## Component Library |
| |
| Interactive documentation for Superset's UI component library. **${totalComponents} components** documented across ${Object.keys(categories).filter(k => categories[k].length > 0).length} categories. |
| |
| ${categoryList} |
| |
| ## Usage |
| |
| All components are exported from \`@superset-ui/core/components\`: |
| |
| \`\`\`tsx |
| import { Button, Modal, Select } from '@superset-ui/core/components'; |
| \`\`\` |
| |
| ## Contributing |
| |
| This documentation is auto-generated from Storybook stories. To add or update component documentation: |
| |
| 1. Create or update the component's \`.stories.tsx\` file |
| 2. Add a descriptive \`title\` and \`description\` in the story meta |
| 3. Export an interactive story with \`args\` for configurable props |
| 4. Run \`yarn generate:superset-components\` in the \`docs/\` directory |
| |
| :::info Work in Progress |
| This component library is actively being documented. See the [Components TODO](./TODO) page for a list of components awaiting documentation. |
| ::: |
| |
| --- |
| |
| *Auto-generated from Storybook stories in the [Design System/Introduction](https://github.com/apache/superset/blob/master/superset-frontend/packages/superset-ui-core/src/components/DesignSystem.stories.tsx) story.* |
| `; |
| } |
| |
| /** |
| * Generate TODO.md tracking skipped components |
| */ |
| function generateTodoMd(skippedFiles) { |
| const disabledSources = SOURCES.filter(s => !s.enabled); |
| const grouped = {}; |
| for (const file of skippedFiles) { |
| const source = disabledSources.find(s => file.includes(s.path)); |
| const sourceName = source ? source.name : 'unknown'; |
| if (!grouped[sourceName]) grouped[sourceName] = []; |
| grouped[sourceName].push(file); |
| } |
| |
| const sections = Object.entries(grouped) |
| .map(([source, files]) => { |
| const fileList = files.map(f => `- [ ] \`${path.relative(ROOT_DIR, f)}\``).join('\n'); |
| return `### ${source}\n\n${files.length} components\n\n${fileList}`; |
| }) |
| .join('\n\n'); |
| |
| return `--- |
| title: Components TODO |
| sidebar_class_name: hidden |
| --- |
| |
| # Components TODO |
| |
| These components were found but not yet supported for documentation generation. |
| Future phases will add support for these sources. |
| |
| ## Summary |
| |
| - **Total skipped:** ${skippedFiles.length} story files |
| - **Reason:** Import path resolution not yet implemented |
| |
| ## Skipped by Source |
| |
| ${sections} |
| |
| ## How to Add Support |
| |
| 1. Determine the correct import path for the source |
| 2. Update \`generate-superset-components.mjs\` to handle the source |
| 3. Add source to \`SUPPORTED_SOURCES\` array |
| 4. Re-run the generator |
| |
| --- |
| |
| *Auto-generated by generate-superset-components.mjs* |
| `; |
| } |
| |
| /** |
| * Main function |
| */ |
| async function main() { |
| console.log('Generating Superset Components documentation...\n'); |
| |
| // Find enabled story files |
| const enabledFiles = findEnabledStoryFiles(); |
| console.log(`Found ${enabledFiles.length} story files from enabled sources\n`); |
| |
| // Find disabled story files (for tracking) |
| const disabledFiles = findDisabledStoryFiles(); |
| console.log(`Found ${disabledFiles.length} story files from disabled sources (tracking only)\n`); |
| |
| // Parse enabled files |
| const components = []; |
| for (const { file, source } of enabledFiles) { |
| const parsed = parseStoryFile(file, source); |
| if (parsed && parsed.componentName) { |
| components.push(parsed); |
| } |
| } |
| |
| console.log(`Parsed ${components.length} components\n`); |
| |
| // Group by category |
| const categories = {}; |
| for (const component of components) { |
| if (!categories[component.category]) { |
| categories[component.category] = []; |
| } |
| categories[component.category].push(component); |
| } |
| |
| // Ensure output directory exists |
| if (!fs.existsSync(OUTPUT_DIR)) { |
| fs.mkdirSync(OUTPUT_DIR, { recursive: true }); |
| } |
| |
| // Generate MDX files by category |
| let generatedCount = 0; |
| for (const [category, categoryComponents] of Object.entries(categories)) { |
| const categoryDir = path.join(OUTPUT_DIR, category); |
| if (!fs.existsSync(categoryDir)) { |
| fs.mkdirSync(categoryDir, { recursive: true }); |
| } |
| |
| // Generate component pages |
| for (const component of categoryComponents) { |
| const storyContent = fs.readFileSync(component.filePath, 'utf-8'); |
| const mdxContent = generateMDX(component, storyContent); |
| const outputPath = path.join(categoryDir, `${component.componentName.toLowerCase()}.mdx`); |
| fs.writeFileSync(outputPath, mdxContent); |
| console.log(` ✓ ${category}/${component.componentName}`); |
| generatedCount++; |
| } |
| |
| // Generate category index |
| const indexContent = generateCategoryIndex(category, categoryComponents); |
| const indexPath = path.join(categoryDir, 'index.mdx'); |
| fs.writeFileSync(indexPath, indexContent); |
| console.log(` ✓ ${category}/index`); |
| } |
| |
| // Generate main overview |
| const overviewContent = generateOverviewIndex(categories); |
| const overviewPath = path.join(OUTPUT_DIR, 'index.mdx'); |
| fs.writeFileSync(overviewPath, overviewContent); |
| console.log(` ✓ index (overview)`); |
| |
| // Generate TODO.md |
| const todoContent = generateTodoMd(disabledFiles); |
| const todoPath = path.join(OUTPUT_DIR, 'TODO.md'); |
| fs.writeFileSync(todoPath, todoContent); |
| console.log(` ✓ TODO.md`); |
| |
| console.log(`\nDone! Generated ${generatedCount} component pages.`); |
| console.log(`Tracked ${disabledFiles.length} components for future implementation.`); |
| } |
| |
| main().catch(console.error); |