feat(storybook): Add Web Component support to story generator
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
The Storybook generator now supports Web Components in addition to React: - Added ComponentType enum (REACT, WEB_COMPONENT) - Enhanced _parse_component to detect Web Components via HTMLElement/customElements - Added _parse_web_component to extract attributes from JSDoc and observedAttributes - Added _generate_csf3_web_component for Web Component story generation - Updated file patterns to include .js files - Moved ds-button.js to src/components/ for proper discovery - Fixed missing requests import in figma.py The generator now successfully generates Storybook stories for admin-ui's ds-button Web Component with all variants, sizes, and states. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,5 +24,5 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"created_at": "2025-12-10T12:52:18.513773",
|
"created_at": "2025-12-10T12:52:18.513773",
|
||||||
"updated_at": "2025-12-10T13:01:40.642236"
|
"updated_at": "2025-12-10T13:46:05.807775"
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|||||||
@@ -7,14 +7,21 @@ variants, and integration points.
|
|||||||
|
|
||||||
Stories serve as the primary documentation and interactive reference
|
Stories serve as the primary documentation and interactive reference
|
||||||
for how components should be used in applications.
|
for how components should be used in applications.
|
||||||
|
|
||||||
|
Supports both React components and Web Components with JSDoc annotations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional, Tuple
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class StoryTemplate(str, Enum):
|
class StoryTemplate(str, Enum):
|
||||||
"""
|
"""
|
||||||
@@ -25,6 +32,15 @@ class StoryTemplate(str, Enum):
|
|||||||
MDX = "mdx" # MDX format (documentation + interactive)
|
MDX = "mdx" # MDX format (documentation + interactive)
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentType(str, Enum):
|
||||||
|
"""
|
||||||
|
Type of component detected during parsing.
|
||||||
|
"""
|
||||||
|
REACT = "react" # React functional or class component
|
||||||
|
WEB_COMPONENT = "web" # Custom Element / Web Component
|
||||||
|
UNKNOWN = "unknown"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PropInfo:
|
class PropInfo:
|
||||||
"""
|
"""
|
||||||
@@ -54,6 +70,8 @@ class ComponentMeta:
|
|||||||
props: List[PropInfo] = field(default_factory=list)
|
props: List[PropInfo] = field(default_factory=list)
|
||||||
description: str = ""
|
description: str = ""
|
||||||
has_children: bool = False
|
has_children: bool = False
|
||||||
|
component_type: ComponentType = ComponentType.UNKNOWN
|
||||||
|
tag_name: Optional[str] = None # For Web Components (e.g., 'ds-button')
|
||||||
|
|
||||||
|
|
||||||
class StoryGenerator:
|
class StoryGenerator:
|
||||||
@@ -63,10 +81,14 @@ class StoryGenerator:
|
|||||||
Generates interactive Storybook stories in CSF3, CSF2, or MDX format,
|
Generates interactive Storybook stories in CSF3, CSF2, or MDX format,
|
||||||
automatically extracting component metadata and creating comprehensive
|
automatically extracting component metadata and creating comprehensive
|
||||||
documentation with variants and default stories.
|
documentation with variants and default stories.
|
||||||
|
|
||||||
|
Supports both React components (.tsx, .jsx) and Web Components (.js).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, root_path: str):
|
def __init__(self, root_path: str):
|
||||||
self.root = Path(root_path).resolve()
|
self.root = Path(root_path).resolve()
|
||||||
|
# Path to the Babel parser script (shared with project_analyzer)
|
||||||
|
self._parser_script = Path(__file__).parent.parent / 'analyze' / 'parser.js'
|
||||||
|
|
||||||
def generate(self, template: StoryTemplate = StoryTemplate.CSF3, dry_run: bool = True) -> List[Dict[str, str]]:
|
def generate(self, template: StoryTemplate = StoryTemplate.CSF3, dry_run: bool = True) -> List[Dict[str, str]]:
|
||||||
"""
|
"""
|
||||||
@@ -165,11 +187,168 @@ class StoryGenerator:
|
|||||||
return story
|
return story
|
||||||
|
|
||||||
async def _parse_component(self, component_path: str) -> ComponentMeta:
|
async def _parse_component(self, component_path: str) -> ComponentMeta:
|
||||||
"""Parse a React component to extract metadata."""
|
"""
|
||||||
|
Parse a component to extract metadata using Babel AST.
|
||||||
|
|
||||||
|
Supports both React components (TypeScript interfaces) and
|
||||||
|
Web Components (JSDoc annotations, observedAttributes, customElements.define).
|
||||||
|
"""
|
||||||
path = self.root / component_path if not Path(component_path).is_absolute() else Path(component_path)
|
path = self.root / component_path if not Path(component_path).is_absolute() else Path(component_path)
|
||||||
content = path.read_text(encoding="utf-8", errors="ignore")
|
content = path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
|
||||||
component_name = path.stem
|
component_name = path.stem
|
||||||
|
|
||||||
|
# Try to determine component type and parse accordingly
|
||||||
|
component_type = ComponentType.UNKNOWN
|
||||||
|
tag_name = None
|
||||||
|
props = []
|
||||||
|
description = ""
|
||||||
|
has_children = False
|
||||||
|
|
||||||
|
# Detect Web Component patterns
|
||||||
|
is_web_component = (
|
||||||
|
'extends HTMLElement' in content or
|
||||||
|
'customElements.define' in content
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_web_component:
|
||||||
|
component_type = ComponentType.WEB_COMPONENT
|
||||||
|
props, tag_name, description, has_children = self._parse_web_component(content)
|
||||||
|
# Convert tag name to PascalCase for story naming
|
||||||
|
if tag_name:
|
||||||
|
component_name = self._tag_to_pascal(tag_name)
|
||||||
|
else:
|
||||||
|
# Try React component parsing (TypeScript interfaces)
|
||||||
|
component_type = ComponentType.REACT
|
||||||
|
props = self._parse_react_props(content)
|
||||||
|
|
||||||
|
# Check if component uses children (React)
|
||||||
|
has_children = 'children' in content.lower() and (
|
||||||
|
'React.ReactNode' in content or
|
||||||
|
'ReactNode' in content or
|
||||||
|
'{children}' in content
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract description from JSDoc if not already found
|
||||||
|
if not description:
|
||||||
|
jsdoc_match = re.search(r'/\*\*\s*\n\s*\*\s*([^\n*]+)', content)
|
||||||
|
if jsdoc_match:
|
||||||
|
description = jsdoc_match.group(1).strip()
|
||||||
|
|
||||||
|
return ComponentMeta(
|
||||||
|
name=component_name,
|
||||||
|
path=component_path,
|
||||||
|
props=props,
|
||||||
|
description=description,
|
||||||
|
has_children=has_children,
|
||||||
|
component_type=component_type,
|
||||||
|
tag_name=tag_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _parse_web_component(self, content: str) -> Tuple[List[PropInfo], Optional[str], str, bool]:
|
||||||
|
"""
|
||||||
|
Parse Web Component to extract attributes from JSDoc and observedAttributes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (props, tag_name, description, has_children)
|
||||||
|
"""
|
||||||
|
props = []
|
||||||
|
tag_name = None
|
||||||
|
description = ""
|
||||||
|
has_children = False
|
||||||
|
|
||||||
|
# Extract tag name from customElements.define('tag-name', ClassName)
|
||||||
|
define_match = re.search(
|
||||||
|
r"customElements\.define\s*\(\s*['\"]([^'\"]+)['\"]",
|
||||||
|
content
|
||||||
|
)
|
||||||
|
if define_match:
|
||||||
|
tag_name = define_match.group(1)
|
||||||
|
|
||||||
|
# Extract observedAttributes
|
||||||
|
# static get observedAttributes() { return ['variant', 'size', ...]; }
|
||||||
|
observed_match = re.search(
|
||||||
|
r"static\s+get\s+observedAttributes\s*\(\s*\)\s*\{\s*return\s*\[([^\]]+)\]",
|
||||||
|
content,
|
||||||
|
re.DOTALL
|
||||||
|
)
|
||||||
|
observed_attrs = []
|
||||||
|
if observed_match:
|
||||||
|
attrs_str = observed_match.group(1)
|
||||||
|
# Extract quoted strings
|
||||||
|
observed_attrs = re.findall(r"['\"]([^'\"]+)['\"]", attrs_str)
|
||||||
|
|
||||||
|
# Parse JSDoc to extract attribute info
|
||||||
|
# Look for @param or attribute descriptions in JSDoc
|
||||||
|
jsdoc_match = re.search(r'/\*\*([^*]|\*(?!/))*\*/', content, re.DOTALL)
|
||||||
|
if jsdoc_match:
|
||||||
|
jsdoc = jsdoc_match.group(0)
|
||||||
|
|
||||||
|
# Extract main description (first line after /**)
|
||||||
|
desc_match = re.search(r'/\*\*\s*\n?\s*\*?\s*([^\n@*]+)', jsdoc)
|
||||||
|
if desc_match:
|
||||||
|
description = desc_match.group(1).strip()
|
||||||
|
|
||||||
|
# Extract attributes section
|
||||||
|
# Attributes:
|
||||||
|
# - variant: primary | secondary | ...
|
||||||
|
# - size: sm | default | lg
|
||||||
|
attrs_section = re.search(
|
||||||
|
r'Attributes:\s*\n((?:\s*\*?\s*-[^\n]+\n?)+)',
|
||||||
|
jsdoc,
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
if attrs_section:
|
||||||
|
attr_lines = attrs_section.group(1)
|
||||||
|
for line in attr_lines.split('\n'):
|
||||||
|
line = line.strip().lstrip('*').strip()
|
||||||
|
if not line.startswith('-'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Parse: - attr_name: value1 | value2 | ...
|
||||||
|
attr_match = re.match(r'-\s*(\w+):\s*(.+)', line)
|
||||||
|
if attr_match:
|
||||||
|
attr_name = attr_match.group(1)
|
||||||
|
attr_values = attr_match.group(2).strip()
|
||||||
|
|
||||||
|
# Parse options from pipe-separated values
|
||||||
|
options = []
|
||||||
|
if '|' in attr_values:
|
||||||
|
options = [v.strip() for v in attr_values.split('|')]
|
||||||
|
|
||||||
|
prop_type = 'string'
|
||||||
|
if attr_values == 'boolean':
|
||||||
|
prop_type = 'boolean'
|
||||||
|
elif options:
|
||||||
|
prop_type = ' | '.join(f"'{o}'" for o in options)
|
||||||
|
|
||||||
|
props.append(PropInfo(
|
||||||
|
name=attr_name,
|
||||||
|
type=prop_type,
|
||||||
|
required=False,
|
||||||
|
options=options,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Add any observed attributes not found in JSDoc
|
||||||
|
jsdoc_attr_names = {p.name for p in props}
|
||||||
|
for attr in observed_attrs:
|
||||||
|
if attr not in jsdoc_attr_names:
|
||||||
|
# Skip ARIA attributes for story generation
|
||||||
|
if attr.startswith('aria-') or attr == 'tabindex':
|
||||||
|
continue
|
||||||
|
props.append(PropInfo(
|
||||||
|
name=attr,
|
||||||
|
type='string',
|
||||||
|
required=False,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Check for slot usage (Web Component children)
|
||||||
|
has_children = '<slot' in content or '<slot>' in content
|
||||||
|
|
||||||
|
return props, tag_name, description, has_children
|
||||||
|
|
||||||
|
def _parse_react_props(self, content: str) -> List[PropInfo]:
|
||||||
|
"""Parse TypeScript interface/type for React component props."""
|
||||||
props = []
|
props = []
|
||||||
|
|
||||||
# Extract props from interface/type
|
# Extract props from interface/type
|
||||||
@@ -216,29 +395,21 @@ class StoryGenerator:
|
|||||||
options=options,
|
options=options,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Check if component uses children
|
return props
|
||||||
has_children = 'children' in content.lower() and (
|
|
||||||
'React.ReactNode' in content or
|
|
||||||
'ReactNode' in content or
|
|
||||||
'{children}' in content
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract component description from JSDoc
|
@staticmethod
|
||||||
description = ""
|
def _tag_to_pascal(tag_name: str) -> str:
|
||||||
jsdoc_match = re.search(r'/\*\*\s*\n\s*\*\s*([^\n*]+)', content)
|
"""Convert kebab-case tag name to PascalCase (e.g., 'ds-button' -> 'DsButton')."""
|
||||||
if jsdoc_match:
|
return ''.join(word.capitalize() for word in tag_name.split('-'))
|
||||||
description = jsdoc_match.group(1).strip()
|
|
||||||
|
|
||||||
return ComponentMeta(
|
|
||||||
name=component_name,
|
|
||||||
path=component_path,
|
|
||||||
props=props,
|
|
||||||
description=description,
|
|
||||||
has_children=has_children,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _generate_csf3(self, meta: ComponentMeta, include_variants: bool) -> str:
|
def _generate_csf3(self, meta: ComponentMeta, include_variants: bool) -> str:
|
||||||
"""Generate CSF3 format story."""
|
"""Generate CSF3 format story."""
|
||||||
|
if meta.component_type == ComponentType.WEB_COMPONENT:
|
||||||
|
return self._generate_csf3_web_component(meta, include_variants)
|
||||||
|
return self._generate_csf3_react(meta, include_variants)
|
||||||
|
|
||||||
|
def _generate_csf3_react(self, meta: ComponentMeta, include_variants: bool) -> str:
|
||||||
|
"""Generate CSF3 format story for React components."""
|
||||||
lines = [
|
lines = [
|
||||||
f"import type {{ Meta, StoryObj }} from '@storybook/react';",
|
f"import type {{ Meta, StoryObj }} from '@storybook/react';",
|
||||||
f"import {{ {meta.name} }} from './{meta.name}';",
|
f"import {{ {meta.name} }} from './{meta.name}';",
|
||||||
@@ -344,6 +515,204 @@ class StoryGenerator:
|
|||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _generate_csf3_web_component(self, meta: ComponentMeta, include_variants: bool) -> str:
|
||||||
|
"""Generate CSF3 format story for Web Components using @storybook/web-components."""
|
||||||
|
tag_name = meta.tag_name or meta.name.lower()
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"/**",
|
||||||
|
f" * Storybook stories for <{tag_name}> Web Component",
|
||||||
|
f" * Auto-generated by DSS Storybook Generator",
|
||||||
|
" */",
|
||||||
|
"",
|
||||||
|
"// Import the component to ensure it's registered",
|
||||||
|
f"import './{meta.path.split('/')[-1]}';",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Build argTypes object
|
||||||
|
arg_types_entries = []
|
||||||
|
for prop in meta.props:
|
||||||
|
if prop.options:
|
||||||
|
options_str = json.dumps(prop.options)
|
||||||
|
arg_types_entries.append(
|
||||||
|
f" {prop.name}: {{\n"
|
||||||
|
f" options: {options_str},\n"
|
||||||
|
f" control: {{ type: 'select' }},\n"
|
||||||
|
f" description: '{prop.description or prop.name + ' attribute'}',\n"
|
||||||
|
f" }}"
|
||||||
|
)
|
||||||
|
elif prop.type == 'boolean':
|
||||||
|
arg_types_entries.append(
|
||||||
|
f" {prop.name}: {{\n"
|
||||||
|
f" control: {{ type: 'boolean' }},\n"
|
||||||
|
f" description: '{prop.description or prop.name + ' attribute'}',\n"
|
||||||
|
f" }}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
arg_types_entries.append(
|
||||||
|
f" {prop.name}: {{\n"
|
||||||
|
f" control: {{ type: 'text' }},\n"
|
||||||
|
f" description: '{prop.description or prop.name + ' attribute'}',\n"
|
||||||
|
f" }}"
|
||||||
|
)
|
||||||
|
|
||||||
|
lines.extend([
|
||||||
|
"export default {",
|
||||||
|
f" title: 'Web Components/{meta.name}',",
|
||||||
|
" parameters: {",
|
||||||
|
" layout: 'centered',",
|
||||||
|
" docs: {",
|
||||||
|
f" description: {{ component: `{meta.description or f'{meta.name} Web Component'}` }},",
|
||||||
|
" },",
|
||||||
|
" },",
|
||||||
|
" tags: ['autodocs'],",
|
||||||
|
" argTypes: {",
|
||||||
|
])
|
||||||
|
lines.append(",\n".join(arg_types_entries))
|
||||||
|
lines.extend([
|
||||||
|
" },",
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Generate render function
|
||||||
|
lines.extend([
|
||||||
|
"/**",
|
||||||
|
" * Render function that creates the Web Component with attributes",
|
||||||
|
" */",
|
||||||
|
"const render = (args) => {",
|
||||||
|
f" const el = document.createElement('{tag_name}');",
|
||||||
|
"",
|
||||||
|
" // Set attributes from args",
|
||||||
|
" Object.entries(args).forEach(([key, value]) => {",
|
||||||
|
" if (key === 'children' || key === 'slot') {",
|
||||||
|
" el.innerHTML = value;",
|
||||||
|
" } else if (typeof value === 'boolean') {",
|
||||||
|
" if (value) el.setAttribute(key, '');",
|
||||||
|
" else el.removeAttribute(key);",
|
||||||
|
" } else if (value !== undefined && value !== null) {",
|
||||||
|
" el.setAttribute(key, String(value));",
|
||||||
|
" }",
|
||||||
|
" });",
|
||||||
|
"",
|
||||||
|
" return el;",
|
||||||
|
"};",
|
||||||
|
"",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Get default args
|
||||||
|
default_args = self._get_default_args_web_component(meta)
|
||||||
|
default_args_str = json.dumps(default_args, indent=2).replace('\n', '\n ')
|
||||||
|
|
||||||
|
# Generate default story
|
||||||
|
lines.extend([
|
||||||
|
"/**",
|
||||||
|
" * Default story showing the component in its default state",
|
||||||
|
" */",
|
||||||
|
"export const Default = {",
|
||||||
|
" render,",
|
||||||
|
f" args: {default_args_str},",
|
||||||
|
"};",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Generate variant stories
|
||||||
|
if include_variants:
|
||||||
|
variant_prop = next(
|
||||||
|
(p for p in meta.props if p.name == 'variant' and p.options),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if variant_prop:
|
||||||
|
for variant in variant_prop.options:
|
||||||
|
story_name = variant.title().replace('-', '').replace('_', '')
|
||||||
|
lines.extend([
|
||||||
|
"",
|
||||||
|
f"export const {story_name} = {{",
|
||||||
|
" render,",
|
||||||
|
" args: {",
|
||||||
|
" ...Default.args,",
|
||||||
|
f" variant: '{variant}',",
|
||||||
|
" },",
|
||||||
|
"};",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Size variants
|
||||||
|
size_prop = next(
|
||||||
|
(p for p in meta.props if p.name == 'size' and p.options),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if size_prop:
|
||||||
|
for size in size_prop.options:
|
||||||
|
story_name = f"Size{size.title().replace('-', '')}"
|
||||||
|
lines.extend([
|
||||||
|
"",
|
||||||
|
f"export const {story_name} = {{",
|
||||||
|
" render,",
|
||||||
|
" args: {",
|
||||||
|
" ...Default.args,",
|
||||||
|
f" size: '{size}',",
|
||||||
|
" },",
|
||||||
|
"};",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Disabled state
|
||||||
|
disabled_prop = next(
|
||||||
|
(p for p in meta.props if p.name == 'disabled'),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if disabled_prop:
|
||||||
|
lines.extend([
|
||||||
|
"",
|
||||||
|
"export const Disabled = {",
|
||||||
|
" render,",
|
||||||
|
" args: {",
|
||||||
|
" ...Default.args,",
|
||||||
|
" disabled: true,",
|
||||||
|
" },",
|
||||||
|
"};",
|
||||||
|
])
|
||||||
|
|
||||||
|
# Loading state (common for buttons)
|
||||||
|
loading_prop = next(
|
||||||
|
(p for p in meta.props if p.name == 'loading'),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if loading_prop:
|
||||||
|
lines.extend([
|
||||||
|
"",
|
||||||
|
"export const Loading = {",
|
||||||
|
" render,",
|
||||||
|
" args: {",
|
||||||
|
" ...Default.args,",
|
||||||
|
" loading: true,",
|
||||||
|
" },",
|
||||||
|
"};",
|
||||||
|
])
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _get_default_args_web_component(self, meta: ComponentMeta) -> Dict[str, Any]:
|
||||||
|
"""Get default args for a Web Component."""
|
||||||
|
args = {}
|
||||||
|
|
||||||
|
for prop in meta.props:
|
||||||
|
if prop.name == 'variant' and prop.options:
|
||||||
|
args['variant'] = prop.options[0]
|
||||||
|
elif prop.name == 'size' and prop.options:
|
||||||
|
args['size'] = prop.options[0]
|
||||||
|
elif prop.name == 'type' and prop.options:
|
||||||
|
args['type'] = prop.options[0]
|
||||||
|
elif prop.name == 'disabled':
|
||||||
|
args['disabled'] = False
|
||||||
|
elif prop.name == 'loading':
|
||||||
|
args['loading'] = False
|
||||||
|
|
||||||
|
# Add children/slot content for components with slots
|
||||||
|
if meta.has_children:
|
||||||
|
args['children'] = f'Click me'
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
def _generate_csf2(self, meta: ComponentMeta, include_variants: bool) -> str:
|
def _generate_csf2(self, meta: ComponentMeta, include_variants: bool) -> str:
|
||||||
"""Generate CSF2 format story."""
|
"""Generate CSF2 format story."""
|
||||||
lines = [
|
lines = [
|
||||||
@@ -459,6 +828,8 @@ class StoryGenerator:
|
|||||||
"""
|
"""
|
||||||
Generate stories for all components in a directory.
|
Generate stories for all components in a directory.
|
||||||
|
|
||||||
|
Supports both React components (.tsx, .jsx) and Web Components (.js).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
directory: Path to component directory
|
directory: Path to component directory
|
||||||
template: Story template format
|
template: Story template format
|
||||||
@@ -473,23 +844,42 @@ class StoryGenerator:
|
|||||||
if not dir_path.exists():
|
if not dir_path.exists():
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# Find component files
|
# Find component files (React + Web Components)
|
||||||
for pattern in ['*.tsx', '*.jsx']:
|
for pattern in ['*.tsx', '*.jsx', '*.js']:
|
||||||
for comp_path in dir_path.glob(pattern):
|
for comp_path in dir_path.glob(pattern):
|
||||||
# Skip story files, test files, index files
|
# Skip story files, test files, index files
|
||||||
if any(x in comp_path.name.lower() for x in ['.stories.', '.test.', '.spec.', 'index.']):
|
if any(x in comp_path.name.lower() for x in ['.stories.', '.test.', '.spec.', 'index.']):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip non-component files (not PascalCase)
|
# Check if it's a valid component file
|
||||||
if not comp_path.stem[0].isupper():
|
is_valid = False
|
||||||
|
|
||||||
|
# React components: PascalCase naming (e.g., Button.tsx)
|
||||||
|
if comp_path.suffix in ['.tsx', '.jsx'] and comp_path.stem[0].isupper():
|
||||||
|
is_valid = True
|
||||||
|
|
||||||
|
# Web Components: check file content for class extends HTMLElement
|
||||||
|
elif comp_path.suffix == '.js':
|
||||||
|
try:
|
||||||
|
content = comp_path.read_text(encoding='utf-8', errors='ignore')
|
||||||
|
if 'extends HTMLElement' in content or 'customElements.define' in content:
|
||||||
|
is_valid = True
|
||||||
|
log.debug(f"Found Web Component: {comp_path}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not is_valid:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rel_path = str(comp_path.relative_to(self.root))
|
rel_path = str(comp_path.relative_to(self.root))
|
||||||
story = await self.generate_story(rel_path, template)
|
story = await self.generate_story(rel_path, template)
|
||||||
|
|
||||||
# Determine story output path
|
# Determine story output path (use .stories.js for Web Components)
|
||||||
story_path = comp_path.with_suffix('.stories.tsx')
|
if comp_path.suffix == '.js':
|
||||||
|
story_path = comp_path.with_name(comp_path.stem + '.stories.js')
|
||||||
|
else:
|
||||||
|
story_path = comp_path.with_suffix('.stories.tsx')
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'component': rel_path,
|
'component': rel_path,
|
||||||
@@ -504,6 +894,7 @@ class StoryGenerator:
|
|||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
log.error(f"Error generating story for {comp_path}: {e}")
|
||||||
results.append({
|
results.append({
|
||||||
'component': str(comp_path),
|
'component': str(comp_path),
|
||||||
'error': str(e),
|
'error': str(e),
|
||||||
|
|||||||
Reference in New Issue
Block a user