From 5833e6b07543c5ce1b770fce458f0e975a31f665 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Fri, 20 Feb 2026 16:41:02 +0700 Subject: [PATCH] Fix manifest generation for stories without explicit title in meta When a story file has no explicit `title` in its meta (the common case), the CSF parser received 'No title' as fallback, producing wrong story IDs (e.g. `no-title--logged-in` instead of `header--logged-in`). These IDs didn't match the index entries, so `extractStories` filtered them all out, resulting in empty `stories: []` in the manifest. Use `entry.title` from the story index as fallback instead. --- .../src/componentManifest/generator.test.ts | 83 +++++++++++++++++++ .../react/src/componentManifest/generator.ts | 2 +- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index 1bf32627f73d..f170d0d01887 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -575,6 +575,89 @@ test('should create component manifest when only attached-mdx docs have manifest `); }); +test('stories are populated when meta has no explicit title', async () => { + vol.fromJSON( + { + ['./package.json']: JSON.stringify({ name: 'some-package' }), + ['./src/stories/Card.stories.ts']: dedent` + import type { Meta, StoryObj } from '@storybook/react'; + import { Card } from './Card'; + + const meta: Meta = { + component: Card, + }; + export default meta; + type Story = StoryObj; + + export const Default: Story = { args: { label: 'Click me' } }; + export const Large: Story = { args: { label: 'Big button', size: 'large' } }; + `, + ['./src/stories/Card.tsx']: dedent` + import React from 'react'; + export interface CardProps { + label: string; + size?: 'small' | 'large'; + } + + /** A simple card component */ + export const Card = ({ label, size }: CardProps) => { + return
{label}
; + }; + `, + }, + '/app' + ); + + const manifestEntries = [ + { + type: 'story', + subtype: 'story', + id: 'card--default', + name: 'Default', + title: 'Card', + importPath: './src/stories/Card.stories.ts', + componentPath: './src/stories/Card.tsx', + tags: [Tag.DEV, Tag.TEST, Tag.MANIFEST], + exportName: 'Default', + }, + { + type: 'story', + subtype: 'story', + id: 'card--large', + name: 'Large', + title: 'Card', + importPath: './src/stories/Card.stories.ts', + componentPath: './src/stories/Card.tsx', + tags: [Tag.DEV, Tag.TEST, Tag.MANIFEST], + exportName: 'Large', + }, + ]; + + const result = await manifests(undefined, { manifestEntries } as any); + const component = result?.components?.components?.['card']; + + // When no explicit title is in the meta, stories should still be populated + // because the generator should use the index entry's title as fallback + expect(component?.stories).toMatchInlineSnapshot(` + [ + { + "description": undefined, + "id": "card--default", + "name": "Default", + "snippet": "const Default = () => ;", + "summary": undefined, + }, + { + "description": undefined, + "id": "card--large", + "name": "Large", + "snippet": "const Large = () => ;", + "summary": undefined, + }, + ] + `); +}); + test('should extract story description and summary from JSDoc comments', async () => { const code = withCSF3(dedent` /** diff --git a/code/renderers/react/src/componentManifest/generator.ts b/code/renderers/react/src/componentManifest/generator.ts index 1c2115c3a63b..7c98a2e97050 100644 --- a/code/renderers/react/src/componentManifest/generator.ts +++ b/code/renderers/react/src/componentManifest/generator.ts @@ -130,7 +130,7 @@ export const manifests: PresetPropertyFn< (entry as DocsIndexEntry).storiesImports[0]; const absoluteImportPath = path.join(process.cwd(), storyFilePath); const storyFile = cachedReadFileSync(absoluteImportPath, 'utf-8') as string; - const csf = loadCsf(storyFile, { makeTitle: (title) => title ?? 'No title' }).parse(); + const csf = loadCsf(storyFile, { makeTitle: () => entry.title }).parse(); const componentName = csf._meta?.component; const id = entry.id.split('--')[0];