Files
webapp/scripts/generate-stories.mjs
2026-01-07 09:10:35 +07:00

116 lines
3.1 KiB
JavaScript

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<typeof StoryComponent> = {
title: '${title}',
component: StoryComponent,
render: (args) => ({
components: { StoryComponent },
setup() {
return { args }
},
template: '<StoryComponent v-bind=\"args\" />'
})
}
export default meta
type Story = StoryObj<typeof meta>
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)
})