import { promises as fs } from 'node:fs' import path from 'node:path' const componentsRoot = path.resolve(process.cwd(), 'app/components') const storyExtensions = ['.stories.ts', '.stories.js', '.stories.tsx', '.stories.jsx'] const titleCase = (str) => str .split(/[-_/]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join('/') const toIdentifier = (str) => str .split(/[^a-zA-Z0-9]+/) .filter(Boolean) .map((part, idx) => { const lower = part.toLowerCase() if (idx === 0) return lower.charAt(0).toUpperCase() + lower.slice(1) return part.charAt(0).toUpperCase() + part.slice(1) }) .join('') || 'Component' const hasStorySibling = async (dir, baseName) => { const files = await fs.readdir(dir) return files.some((file) => { const match = file.match(/^(.+)\.stories\.(t|j)sx?$/i) if (!match) return false return match[1] === baseName }) } const walkVueFiles = async (dir) => { const entries = await fs.readdir(dir, { withFileTypes: true }) const files = [] for (const entry of entries) { const fullPath = path.join(dir, entry.name) if (entry.isDirectory()) { files.push(...(await walkVueFiles(fullPath))) continue } if (entry.isFile() && entry.name.endsWith('.vue')) { files.push(fullPath) } } return files } const buildStoryContent = (filePath) => { const baseName = path.basename(filePath, '.vue') const importPath = `./${path.basename(filePath)}` const relative = path.relative(componentsRoot, filePath).replace(/\\/g, '/') const title = titleCase(relative.replace('.vue', '')) const componentId = toIdentifier(baseName) return `import type { Meta, StoryObj } from '@storybook/vue3' import StoryComponent from '${importPath}' const meta: Meta = { title: '${title}', component: StoryComponent, render: (args) => ({ components: { StoryComponent }, setup() { return { args } }, template: '' }) } export default meta type Story = StoryObj export const Primary: Story = { args: {} } ` } const ensureDir = async (dir) => { await fs.mkdir(dir, { recursive: true }) } const run = async () => { const vueFiles = await walkVueFiles(componentsRoot) const generated = [] for (const filePath of vueFiles) { const dir = path.dirname(filePath) const baseName = path.basename(filePath, '.vue') const storyExists = await hasStorySibling(dir, baseName) if (storyExists) continue const storyPath = path.join(dir, `${baseName}.stories.ts`) await ensureDir(dir) const content = buildStoryContent(filePath) await fs.writeFile(storyPath, content, 'utf8') generated.push(path.relative(process.cwd(), storyPath)) } if (generated.length === 0) { console.log('No new stories generated. All components already have stories.') return } console.log(`Generated ${generated.length} stories:`) generated.forEach((p) => console.log(`- ${p}`)) } run().catch((err) => { console.error('Failed to generate stories', err) process.exit(1) })