39 lines
4.2 KiB
JavaScript
39 lines
4.2 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import { loadCsf } from 'storybook/internal/csf-tools'
|
|
|
|
// Run from the Track B root: node .storybook/qa/generate-inventory.mjs
|
|
// Only writes Storybook documentation; never renames or removes stories.
|
|
const root = process.cwd()
|
|
if (!fs.existsSync(path.join(root, '.storybook/main.ts'))) throw new Error('Run from the HMDesign repository root')
|
|
const walk = dir => fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)])
|
|
const files = walk(path.join(root, 'src')).filter(f => /\.stories\.tsx?$/.test(f)).sort()
|
|
const inventory = files.map(file => {
|
|
const csf = loadCsf(fs.readFileSync(file, 'utf8'), { makeTitle: title => title }).parse()
|
|
const title = csf.meta.title
|
|
if (!title) throw new Error(`Missing meta.title: ${file}`)
|
|
const metaTags = csf.meta.tags ?? []
|
|
const stories = Object.entries(csf._stories).map(([exportName, story]) => {
|
|
const storyTags = story.tags ?? []
|
|
const effectiveTags = [...new Set([...metaTags, ...storyTags])]
|
|
const utility = /Playground|Compatibility/.test(exportName) || /Verification$/.test(exportName) || exportName === 'Verification'
|
|
const category = utility || effectiveTags.includes('internal') ? 'UTILITY' : effectiveTags.includes('verification') ? 'VERIFICATION' : 'PRIMARY'
|
|
const duplicateCandidate = effectiveTags.includes('verification') && /^(Default|Sizes|Roles|Variants|States|Basic|Selected|Disabled|Horizontal|Vertical)$/.test(exportName)
|
|
return { id: story.id, exportName, name: story.name, storyTags, effectiveTags, category, decision: effectiveTags.some(tag => ['verification', 'internal'].includes(tag)) ? 'HIDE' : 'KEEP', candidate: duplicateCandidate ? 'MERGE_CANDIDATE' : null, reason: duplicateCandidate ? 'Representative board may cover the same specimen; keep direct QA until behavioral equivalence is reviewed.' : effectiveTags.some(tag => ['verification', 'internal'].includes(tag)) ? 'Keep direct URL and tag-filter access; excluded from default sidebar.' : 'Representative story or distinct reference page.' }
|
|
})
|
|
return { file: path.relative(root, file).replaceAll('\\', '/'), title, metaTags, stories }
|
|
})
|
|
const all = inventory.flatMap(f => f.stories)
|
|
const summary = { storyFiles: files.length, stories: all.length, categories: Object.fromEntries(['PRIMARY', 'VERIFICATION', 'UTILITY', 'LEGACY / DUPLICATE'].map(c => [c, all.filter(s => s.category === c).length])), decisions: Object.fromEntries(['KEEP', 'HIDE', 'MERGE_CANDIDATE', 'DELETE_CANDIDATE'].map(c => [c, all.filter(s => s.decision === c || s.candidate === c).length])), hierarchy: ['Foundation', 'Components', 'Patterns', 'Center Graphic', 'Theme'] }
|
|
const output = path.join(root, '.storybook', 'qa')
|
|
fs.mkdirSync(output, { recursive: true })
|
|
fs.writeFileSync(path.join(output, 'story-inventory.json'), JSON.stringify({ summary, inventory }, null, 2) + '\n')
|
|
const lines = ['# Track B Story inventory', '', 'Generated from every `src/**/*.stories.ts(x)` using the installed Storybook CSF parser. Run `node .storybook/qa/generate-inventory.mjs` from Track B to refresh.', '', 'This is a classification record. No Story IDs, exports or titles are renamed. HIDE uses the existing verification filter, not deletion. MERGE_CANDIDATE is advisory and can coexist with HIDE. Utility includes executable checks and compatibility/playground tools. No legacy or delete candidate is asserted without evidence.', '', '## Summary', '', ...Object.entries(summary.categories).map(([k,v]) => `- ${k}: ${v}`), ...Object.entries(summary.decisions).map(([k,v]) => `- ${k}: ${v}`), `- Story files: ${summary.storyFiles}`, `- Stories: ${summary.stories}`, `- Hierarchy: ${summary.hierarchy.join(' → ')}`, '', '## Full inventory', '']
|
|
for (const f of inventory) {
|
|
lines.push(`### ${f.title}`, '', `Source: \`${f.file}\`; meta.tags: ${JSON.stringify(f.metaTags)}`, '')
|
|
for (const s of f.stories) lines.push(`- ${s.exportName} — ${s.name}; tags: ${JSON.stringify(s.effectiveTags)}; ${s.category}; ${s.decision}${s.candidate ? `; ${s.candidate}` : ''}. ${s.reason}`)
|
|
lines.push('')
|
|
}
|
|
fs.writeFileSync(path.join(output, 'STORY-INVENTORY.md'), lines.join('\n'))
|
|
console.log(JSON.stringify(summary))
|