116 lines
3.1 KiB
JavaScript
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)
|
|
})
|