mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
677 lines
22 KiB
JavaScript
677 lines
22 KiB
JavaScript
/**
|
|
* 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);
|