mirror of
https://github.com/apache/superset.git
synced 2026-04-07 18:35:15 +00:00
feat(frontend): upgrade Storybook and add extension component documentation (#36498)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
676
docs/scripts/generate-extension-components.mjs
Normal file
676
docs/scripts/generate-extension-components.mjs
Normal file
@@ -0,0 +1,676 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This script scans for Storybook stories in superset-core/src and generates
|
||||
* MDX documentation pages for the developer portal. All components in
|
||||
* superset-core are considered extension-compatible by virtue of their location.
|
||||
*
|
||||
* Usage: node scripts/generate-extension-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/extensions/components'
|
||||
);
|
||||
const TYPES_OUTPUT_DIR = path.join(DOCS_DIR, 'src/types/apache-superset-core');
|
||||
const TYPES_OUTPUT_PATH = path.join(TYPES_OUTPUT_DIR, 'index.d.ts');
|
||||
const SUPERSET_CORE_DIR = path.join(
|
||||
ROOT_DIR,
|
||||
'superset-frontend/packages/superset-core'
|
||||
);
|
||||
|
||||
/**
|
||||
* Find all story files in the superset-core package
|
||||
*/
|
||||
async function findStoryFiles() {
|
||||
const files = [];
|
||||
|
||||
// Use fs to recursively find files since glob might not be available
|
||||
function walkDir(dir) {
|
||||
if (!fs.existsSync(dir)) return;
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
walkDir(fullPath);
|
||||
} else if (entry.name.endsWith('.stories.tsx')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walkDir(path.join(SUPERSET_CORE_DIR, 'src'));
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a story file and extract metadata
|
||||
*
|
||||
* All stories in superset-core are considered extension-compatible
|
||||
* by virtue of their location - no tag needed.
|
||||
*/
|
||||
function parseStoryFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Extract component name from title
|
||||
const titleMatch = content.match(/title:\s*['"]([^'"]+)['"]/);
|
||||
const title = titleMatch ? titleMatch[1] : null;
|
||||
|
||||
// Extract component name (last part of title path)
|
||||
const componentName = title ? title.split('/').pop() : null;
|
||||
|
||||
// Extract description from parameters
|
||||
// Handle concatenated strings like: 'part1 ' + 'part2'
|
||||
let description = '';
|
||||
|
||||
// First try to find the description block
|
||||
const descBlockMatch = content.match(
|
||||
/description:\s*{\s*component:\s*([\s\S]*?)\s*},?\s*}/
|
||||
);
|
||||
|
||||
if (descBlockMatch) {
|
||||
const descBlock = descBlockMatch[1];
|
||||
// Extract all string literals and concatenate them
|
||||
const stringParts = [];
|
||||
const stringMatches = descBlock.matchAll(/['"]([^'"]*)['"]/g);
|
||||
for (const match of stringMatches) {
|
||||
stringParts.push(match[1]);
|
||||
}
|
||||
description = stringParts.join('').trim();
|
||||
}
|
||||
|
||||
// Extract package info
|
||||
const packageMatch = content.match(/package:\s*['"]([^'"]+)['"]/);
|
||||
const packageName = packageMatch ? packageMatch[1] : '@apache-superset/core/ui';
|
||||
|
||||
// Extract import path - handle double-quoted strings containing single quotes
|
||||
// Match: importPath: "import { Alert } from '@apache-superset/core';"
|
||||
const importMatchDouble = content.match(/importPath:\s*"([^"]+)"/);
|
||||
const importMatchSingle = content.match(/importPath:\s*'([^']+)'/);
|
||||
let importPath = `import { ${componentName} } from '${packageName}';`;
|
||||
if (importMatchDouble) {
|
||||
importPath = importMatchDouble[1];
|
||||
} else if (importMatchSingle) {
|
||||
importPath = importMatchSingle[1];
|
||||
}
|
||||
|
||||
// Get the directory containing the story to find the component
|
||||
const storyDir = path.dirname(filePath);
|
||||
const componentFile = path.join(storyDir, 'index.tsx');
|
||||
const hasComponentFile = fs.existsSync(componentFile);
|
||||
|
||||
// Try to extract props interface from component file (for future use)
|
||||
if (hasComponentFile) {
|
||||
// Read component file - props extraction reserved for future enhancement
|
||||
// const componentContent = fs.readFileSync(componentFile, 'utf-8');
|
||||
}
|
||||
|
||||
// Extract story exports (named exports that aren't the default)
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
filePath,
|
||||
title,
|
||||
componentName,
|
||||
description,
|
||||
packageName,
|
||||
importPath,
|
||||
storyExports,
|
||||
hasComponentFile,
|
||||
relativePath: path.relative(ROOT_DIR, filePath),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract argTypes/args from story content for generating controls
|
||||
*/
|
||||
function extractArgsAndControls(content, componentName, storyContent) {
|
||||
// Look for InteractiveX.args pattern - handle multi-line objects
|
||||
const argsMatch = content.match(
|
||||
new RegExp(`Interactive${componentName}\\.args\\s*=\\s*\\{([\\s\\S]*?)\\};`, 's')
|
||||
);
|
||||
|
||||
// Look for argTypes
|
||||
const argTypesMatch = content.match(
|
||||
new RegExp(`Interactive${componentName}\\.argTypes\\s*=\\s*\\{([\\s\\S]*?)\\};`, 's')
|
||||
);
|
||||
|
||||
const args = {};
|
||||
const controls = [];
|
||||
const propDescriptions = {};
|
||||
|
||||
if (argsMatch) {
|
||||
// Parse args - handle strings, booleans, numbers
|
||||
// Note: Using simple regex without escape handling for security (avoids ReDoS)
|
||||
// This is sufficient for Storybook args which rarely contain escaped quotes
|
||||
const argsContent = argsMatch[1];
|
||||
const argLines = argsContent.matchAll(/(\w+):\s*(['"]([^'"]*)['"']|true|false|\d+)/g);
|
||||
for (const match of argLines) {
|
||||
const key = match[1];
|
||||
let value = match[2];
|
||||
// Convert string booleans
|
||||
if (value === 'true') value = true;
|
||||
else if (value === 'false') value = false;
|
||||
else if (!isNaN(Number(value))) value = Number(value);
|
||||
else if (match[3] !== undefined) value = match[3]; // Use captured string content
|
||||
args[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (argTypesMatch) {
|
||||
const argTypesContent = argTypesMatch[1];
|
||||
|
||||
// Match each top-level property in argTypes
|
||||
// Pattern: propertyName: { ... }, (with balanced braces)
|
||||
const propPattern = /(\w+):\s*\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g;
|
||||
let propMatch;
|
||||
|
||||
while ((propMatch = propPattern.exec(argTypesContent)) !== null) {
|
||||
const name = propMatch[1];
|
||||
const propContent = propMatch[2];
|
||||
|
||||
// Extract description if present
|
||||
const descMatch = propContent.match(/description:\s*['"]([^'"]+)['"]/);
|
||||
if (descMatch) {
|
||||
propDescriptions[name] = descMatch[1];
|
||||
}
|
||||
|
||||
// Skip if it's an action (not a control)
|
||||
if (propContent.includes('action:')) continue;
|
||||
|
||||
// Extract label for display
|
||||
const label = name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1');
|
||||
|
||||
// Check for select control
|
||||
if (propContent.includes("type: 'select'") || propContent.includes('type: "select"')) {
|
||||
// Look for options - could be inline array or variable reference
|
||||
const inlineOptionsMatch = propContent.match(/options:\s*\[([^\]]+)\]/);
|
||||
const varOptionsMatch = propContent.match(/options:\s*(\w+)/);
|
||||
|
||||
let options = [];
|
||||
if (inlineOptionsMatch) {
|
||||
options = [...inlineOptionsMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
|
||||
} else if (varOptionsMatch && storyContent) {
|
||||
// Look up the variable
|
||||
const varName = varOptionsMatch[1];
|
||||
const varDefMatch = storyContent.match(
|
||||
new RegExp(`const\\s+${varName}[^=]*=\\s*\\[([^\\]]+)\\]`)
|
||||
);
|
||||
if (varDefMatch) {
|
||||
options = [...varDefMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
controls.push({ name, label, type: 'select', options });
|
||||
}
|
||||
}
|
||||
// Check for boolean control
|
||||
else if (propContent.includes("type: 'boolean'") || propContent.includes('type: "boolean"')) {
|
||||
controls.push({ name, label, type: 'boolean' });
|
||||
}
|
||||
// Check for text/string control (default for props in args without explicit control)
|
||||
else if (args[name] !== undefined && typeof args[name] === 'string') {
|
||||
controls.push({ name, label, type: 'text' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add text controls for string args that don't have explicit argTypes
|
||||
for (const [key, value] of Object.entries(args)) {
|
||||
if (typeof value === 'string' && !controls.find(c => c.name === key)) {
|
||||
const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1');
|
||||
controls.push({ name: key, label, type: 'text' });
|
||||
}
|
||||
}
|
||||
|
||||
return { args, controls, propDescriptions };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate MDX content for a component
|
||||
*/
|
||||
function generateMDX(component, storyContent) {
|
||||
const { componentName, description, importPath, packageName, relativePath } =
|
||||
component;
|
||||
|
||||
// Extract args, controls, and descriptions from the story
|
||||
const { args, controls, propDescriptions } = extractArgsAndControls(storyContent, componentName, storyContent);
|
||||
|
||||
// Generate the controls array for StoryWithControls
|
||||
const controlsJson = JSON.stringify(controls, null, 2)
|
||||
.replace(/"(\w+)":/g, '$1:') // Remove quotes from keys
|
||||
.replace(/"/g, "'"); // Use single quotes for strings
|
||||
|
||||
// Generate default props
|
||||
const propsJson = JSON.stringify(args, null, 2)
|
||||
.replace(/"(\w+)":/g, '$1:')
|
||||
.replace(/"/g, "'");
|
||||
|
||||
// Generate a realistic live code example from the actual args
|
||||
const liveExampleProps = Object.entries(args)
|
||||
.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(args).map(([key, value]) => {
|
||||
const type = typeof value === 'boolean' ? 'boolean' : typeof value === 'string' ? 'string' : 'any';
|
||||
const desc = propDescriptions[key] || key.charAt(0).toUpperCase() + key.slice(1).replace(/([A-Z])/g, ' $1');
|
||||
return `| \`${key}\` | \`${type}\` | \`${JSON.stringify(value)}\` | ${desc} |`;
|
||||
}).join('\n');
|
||||
|
||||
// Generate usage example props (simplified for readability)
|
||||
const usageExampleProps = Object.entries(args)
|
||||
.slice(0, 3) // Show first 3 props for brevity
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'string') return `${key}="${value}"`;
|
||||
if (typeof value === 'boolean') return value ? key : `${key}={false}`;
|
||||
return `${key}={${JSON.stringify(value)}}`;
|
||||
})
|
||||
.join('\n ');
|
||||
|
||||
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 } from '../../../src/components/StorybookWrapper';
|
||||
import { ${componentName} } from '@apache-superset/core/ui';
|
||||
|
||||
# ${componentName}
|
||||
|
||||
${description || `The ${componentName} component from the Superset extension API.`}
|
||||
|
||||
## Live Example
|
||||
|
||||
<StoryWithControls
|
||||
component={${componentName}}
|
||||
props={${propsJson}}
|
||||
controls={${controlsJson}}
|
||||
/>
|
||||
|
||||
## Try It
|
||||
|
||||
Edit the code below to experiment with the component:
|
||||
|
||||
\`\`\`tsx live
|
||||
function Demo() {
|
||||
return (
|
||||
<${componentName}
|
||||
${liveExampleProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
${propsTable}
|
||||
|
||||
## Usage in Extensions
|
||||
|
||||
This component is available in the \`${packageName}\` package, which is automatically available to Superset extensions.
|
||||
|
||||
\`\`\`tsx
|
||||
${importPath}
|
||||
|
||||
function MyExtension() {
|
||||
return (
|
||||
<${componentName}
|
||||
${usageExampleProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Source Links
|
||||
|
||||
- [Story file](https://github.com/apache/superset/blob/master/${relativePath})
|
||||
- [Component source](https://github.com/apache/superset/blob/master/${relativePath.replace(/\/[^/]+\.stories\.tsx$/, '/index.tsx')})
|
||||
|
||||
---
|
||||
|
||||
*This page was auto-generated from the component's Storybook story.*
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate index page for extension components
|
||||
*/
|
||||
function generateIndexMDX(components) {
|
||||
const componentList = components
|
||||
.map(c => `- [${c.componentName}](./${c.componentName.toLowerCase()})`)
|
||||
.join('\n');
|
||||
|
||||
return `---
|
||||
title: Extension Components
|
||||
sidebar_label: Overview
|
||||
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.
|
||||
-->
|
||||
|
||||
# Extension Components
|
||||
|
||||
These UI components are available to Superset extension developers through the \`@apache-superset/core/ui\` package. They provide a consistent look and feel with the rest of Superset and are designed to be used in extension panels, views, and other UI elements.
|
||||
|
||||
## Available Components
|
||||
|
||||
${componentList}
|
||||
|
||||
## Usage
|
||||
|
||||
All components are exported from the \`@apache-superset/core/ui\` package:
|
||||
|
||||
\`\`\`tsx
|
||||
import { Alert } from '@apache-superset/core/ui';
|
||||
|
||||
export function MyExtensionPanel() {
|
||||
return (
|
||||
<Alert type="info">
|
||||
Welcome to my extension!
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
## Adding New Components
|
||||
|
||||
Components in \`@apache-superset/core/ui\` are automatically documented here. To add a new extension component:
|
||||
|
||||
1. Add the component to \`superset-frontend/packages/superset-core/src/ui/components/\`
|
||||
2. Export it from \`superset-frontend/packages/superset-core/src/ui/components/index.ts\`
|
||||
3. Create a Storybook story with an \`Interactive\` export:
|
||||
|
||||
\`\`\`tsx
|
||||
export default {
|
||||
title: 'Extension Components/MyComponent',
|
||||
component: MyComponent,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Description of the component...',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InteractiveMyComponent = (args) => <MyComponent {...args} />;
|
||||
|
||||
InteractiveMyComponent.args = {
|
||||
variant: 'primary',
|
||||
disabled: false,
|
||||
};
|
||||
|
||||
InteractiveMyComponent.argTypes = {
|
||||
variant: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary'],
|
||||
},
|
||||
disabled: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
};
|
||||
\`\`\`
|
||||
|
||||
4. Run \`yarn start\` in \`docs/\` - the page generates automatically!
|
||||
|
||||
## Interactive Documentation
|
||||
|
||||
For interactive examples with controls, visit the [Storybook](/storybook/?path=/docs/extension-components--docs).
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract type exports from a component file
|
||||
*/
|
||||
function extractComponentTypes(componentPath) {
|
||||
if (!fs.existsSync(componentPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(componentPath, 'utf-8');
|
||||
const types = [];
|
||||
|
||||
// Find all "export type X = ..." declarations
|
||||
const typeMatches = content.matchAll(/export\s+type\s+(\w+)\s*=\s*([^;]+);/g);
|
||||
for (const match of typeMatches) {
|
||||
types.push({
|
||||
name: match[1],
|
||||
definition: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// Find all "export const X = ..." declarations (components)
|
||||
const constMatches = content.matchAll(/export\s+const\s+(\w+)\s*[=:]/g);
|
||||
const components = [];
|
||||
for (const match of constMatches) {
|
||||
components.push(match[1]);
|
||||
}
|
||||
|
||||
return { types, components };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the type declarations file content
|
||||
*/
|
||||
function generateTypeDeclarations(componentInfos) {
|
||||
const imports = new Set();
|
||||
const typeDeclarations = [];
|
||||
const componentDeclarations = [];
|
||||
|
||||
for (const info of componentInfos) {
|
||||
const componentDir = path.dirname(info.filePath);
|
||||
const componentFile = path.join(componentDir, 'index.tsx');
|
||||
const extracted = extractComponentTypes(componentFile);
|
||||
|
||||
if (!extracted) continue;
|
||||
|
||||
// Check if types reference antd or react
|
||||
for (const type of extracted.types) {
|
||||
if (type.definition.includes('AntdAlertProps') || type.definition.includes('AlertProps')) {
|
||||
imports.add("import type { AlertProps as AntdAlertProps } from 'antd/es/alert';");
|
||||
}
|
||||
if (type.definition.includes('PropsWithChildren') || type.definition.includes('FC')) {
|
||||
imports.add("import type { PropsWithChildren, FC } from 'react';");
|
||||
}
|
||||
|
||||
// Add the type declaration
|
||||
typeDeclarations.push(` export type ${type.name} = ${type.definition};`);
|
||||
}
|
||||
|
||||
// Add component declarations
|
||||
for (const comp of extracted.components) {
|
||||
const propsType = `${comp}Props`;
|
||||
const hasPropsType = extracted.types.some(t => t.name === propsType);
|
||||
if (hasPropsType) {
|
||||
componentDeclarations.push(` export const ${comp}: FC<${propsType}>;`);
|
||||
} else {
|
||||
componentDeclarations.push(` export const ${comp}: FC<Record<string, unknown>>;`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove 'export' prefix for direct exports (not in declare module)
|
||||
const cleanedTypes = typeDeclarations.map(t => t.replace(/^ {2}export /, 'export '));
|
||||
const cleanedComponents = componentDeclarations.map(c => c.replace(/^ {2}export /, 'export '));
|
||||
|
||||
return `/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type declarations for @apache-superset/core/ui
|
||||
*
|
||||
* AUTO-GENERATED by scripts/generate-extension-components.mjs
|
||||
* Do not edit manually - regenerate by running: yarn generate:extension-components
|
||||
*/
|
||||
${Array.from(imports).join('\n')}
|
||||
|
||||
${cleanedTypes.join('\n')}
|
||||
|
||||
${cleanedComponents.join('\n')}
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
*/
|
||||
async function main() {
|
||||
console.log('Scanning for extension-compatible stories...\n');
|
||||
|
||||
// Find all story files
|
||||
const storyFiles = await findStoryFiles();
|
||||
console.log(`Found ${storyFiles.length} story files in superset-core\n`);
|
||||
|
||||
// Parse each story file
|
||||
const components = [];
|
||||
for (const file of storyFiles) {
|
||||
const parsed = parseStoryFile(file);
|
||||
if (parsed) {
|
||||
components.push(parsed);
|
||||
console.log(` ✓ ${parsed.componentName} (${parsed.relativePath})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (components.length === 0) {
|
||||
console.log(
|
||||
'\nNo extension-compatible components found. Make sure stories have:'
|
||||
);
|
||||
console.log(" tags: ['extension-compatible']");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\nFound ${components.length} extension-compatible components\n`);
|
||||
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(OUTPUT_DIR)) {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
console.log(`Created directory: ${OUTPUT_DIR}\n`);
|
||||
}
|
||||
|
||||
// Generate MDX files
|
||||
for (const component of components) {
|
||||
// Read the story content for extracting args/controls
|
||||
const storyContent = fs.readFileSync(component.filePath, 'utf-8');
|
||||
const mdxContent = generateMDX(component, storyContent);
|
||||
const outputPath = path.join(
|
||||
OUTPUT_DIR,
|
||||
`${component.componentName.toLowerCase()}.mdx`
|
||||
);
|
||||
fs.writeFileSync(outputPath, mdxContent);
|
||||
console.log(` Generated: ${path.relative(DOCS_DIR, outputPath)}`);
|
||||
}
|
||||
|
||||
// Generate index page
|
||||
const indexContent = generateIndexMDX(components);
|
||||
const indexPath = path.join(OUTPUT_DIR, 'index.mdx');
|
||||
fs.writeFileSync(indexPath, indexContent);
|
||||
console.log(` Generated: ${path.relative(DOCS_DIR, indexPath)}`);
|
||||
|
||||
// Generate type declarations
|
||||
if (!fs.existsSync(TYPES_OUTPUT_DIR)) {
|
||||
fs.mkdirSync(TYPES_OUTPUT_DIR, { recursive: true });
|
||||
}
|
||||
const typesContent = generateTypeDeclarations(components);
|
||||
fs.writeFileSync(TYPES_OUTPUT_PATH, typesContent);
|
||||
console.log(` Generated: ${path.relative(DOCS_DIR, TYPES_OUTPUT_PATH)}`);
|
||||
|
||||
console.log('\nDone! Extension component documentation generated.');
|
||||
console.log(
|
||||
`\nGenerated ${components.length + 2} files (${components.length + 1} MDX + 1 type declaration)`
|
||||
);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user