Skip to content

Commit 50be8d7

Browse files
committed
Filter by manifest tag and include story descriptions/summaries
1 parent 8151722 commit 50be8d7

File tree

7 files changed

+396
-147
lines changed

7 files changed

+396
-147
lines changed

code/core/src/core-server/manifest.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,8 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) {
774774
<span class="ex-name">${esc(ex.name)}</span>
775775
<span class="badge err">story error</span>
776776
</div>
777+
${ex?.summary ? `<div>${esc(ex.summary)}</div>` : ''}
778+
${ex?.description ? `<div class=\"hint\">${esc(ex.description)}</div>` : ''}
777779
${ex?.snippet ? `<pre><code>${esc(ex.snippet)}</code></pre>` : ''}
778780
${ex?.error?.message ? `<pre><code>${esc(ex.error.message)}</code></pre>` : ''}
779781
</div>`
@@ -800,6 +802,8 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) {
800802
<span class="ex-name">${esc(ex.name)}</span>
801803
<span class="badge ok">story ok</span>
802804
</div>
805+
${ex?.summary ? `<div>${esc(ex.summary)}</div>` : ''}
806+
${ex?.description ? `<div class=\"hint\">${esc(ex.description)}</div>` : ''}
803807
${ex?.snippet ? `<pre><code>${esc(ex.snippet)}</code></pre>` : ''}
804808
</div>`
805809
)

code/core/src/types/modules/core-common.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,13 @@ export interface ComponentManifest {
352352
description?: string;
353353
import?: string;
354354
summary?: string;
355-
stories: { name: string; snippet?: string; error?: { name: string; message: string } }[];
355+
stories: {
356+
name: string;
357+
snippet?: string;
358+
description?: string;
359+
summary?: string;
360+
error?: { name: string; message: string };
361+
}[];
356362
jsDocTags: Record<string, string[]>;
357363
error?: { name: string; message: string };
358364
}

code/renderers/react/src/componentManifest/generator.test.ts

Lines changed: 22 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,16 @@ import { vol } from 'memfs';
66
import { dedent } from 'ts-dedent';
77

88
import { componentManifestGenerator } from './generator';
9+
import { fsMocks } from './test-utils';
910

1011
vi.mock('storybook/internal/common', async (importOriginal) => ({
1112
...(await importOriginal()),
1213
// Keep it simple: hardcode known inputs to expected outputs for this test.
13-
resolveImport: (id: string, opts: { basedir: string }) => {
14-
const basedir = opts?.basedir;
15-
return basedir === '/app/src/stories' && id === './Button'
16-
? './src/stories/Button.tsx'
17-
: basedir === '/app/src/stories' && id === './Header'
18-
? './src/stories/Header.tsx'
19-
: id;
14+
resolveImport: (id: string) => {
15+
return {
16+
'./Button': './src/stories/Button.tsx',
17+
'./Header': './src/stories/Header.tsx',
18+
}[id];
2019
},
2120
JsPackageManagerFactory: {
2221
getPackageManager: () => ({
@@ -120,113 +119,7 @@ const indexJson = {
120119

121120
beforeEach(() => {
122121
vi.spyOn(process, 'cwd').mockReturnValue('/app');
123-
vol.fromJSON(
124-
{
125-
['./package.json']: JSON.stringify({ name: 'some-package' }),
126-
['./src/stories/Button.stories.ts']: dedent`
127-
import type { Meta, StoryObj } from '@storybook/react';
128-
import { fn } from 'storybook/test';
129-
import { Button } from './Button';
130-
131-
const meta = {
132-
component: Button,
133-
args: { onClick: fn() },
134-
} satisfies Meta<typeof Button>;
135-
export default meta;
136-
type Story = StoryObj<typeof meta>;
137-
138-
export const Primary: Story = { args: { primary: true, label: 'Button' } };
139-
export const Secondary: Story = { args: { label: 'Button' } };
140-
export const Large: Story = { args: { size: 'large', label: 'Button' } };
141-
export const Small: Story = { args: { size: 'small', label: 'Button' } };`,
142-
['./src/stories/Button.tsx']: dedent`
143-
import React from 'react';
144-
export interface ButtonProps {
145-
/** Description of primary */
146-
primary?: boolean;
147-
backgroundColor?: string;
148-
size?: 'small' | 'medium' | 'large';
149-
label: string;
150-
onClick?: () => void;
151-
}
152-
153-
/** Primary UI component for user interaction */
154-
export const Button = ({
155-
primary = false,
156-
size = 'medium',
157-
backgroundColor,
158-
label,
159-
...props
160-
}: ButtonProps) => {
161-
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
162-
return (
163-
<button
164-
type="button"
165-
className={['storybook-button', \`storybook-button--\${size}\`, mode].join(' ')}
166-
style={{ backgroundColor }}
167-
{...props}
168-
>
169-
{label}
170-
</button>
171-
);
172-
};`,
173-
['./src/stories/Header.stories.ts']: dedent`
174-
import type { Meta, StoryObj } from '@storybook/react';
175-
import { fn } from 'storybook/test';
176-
import Header from './Header';
177-
178-
/**
179-
* Description from meta and very long.
180-
* @summary Component summary
181-
* @import import { Header } from '@design-system/components/Header';
182-
*/
183-
const meta = {
184-
component: Header,
185-
args: {
186-
onLogin: fn(),
187-
onLogout: fn(),
188-
onCreateAccount: fn(),
189-
}
190-
} satisfies Meta<typeof Header>;
191-
export default meta;
192-
type Story = StoryObj<typeof meta>;
193-
export const LoggedIn: Story = { args: { user: { name: 'Jane Doe' } } };
194-
export const LoggedOut: Story = {};
195-
`,
196-
['./src/stories/Header.tsx']: dedent`
197-
import { Button } from './Button';
198-
199-
export interface HeaderProps {
200-
user?: User;
201-
onLogin?: () => void;
202-
onLogout?: () => void;
203-
onCreateAccount?: () => void;
204-
}
205-
206-
export default ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
207-
<header>
208-
<div className="storybook-header">
209-
<div>
210-
{user ? (
211-
<>
212-
<span className="welcome">
213-
Welcome, <b>{user.name}</b>!
214-
</span>
215-
<Button size="small" onClick={onLogout} label="Log out" />
216-
</>
217-
) : (
218-
<>
219-
<Button size="small" onClick={onLogin} label="Log in" />
220-
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
221-
</>
222-
)}
223-
</div>
224-
</div>
225-
</header>
226-
);`,
227-
},
228-
'/app'
229-
);
122+
vol.fromJSON(fsMocks);
230123
return () => vol.reset();
231124
});
232125

@@ -243,14 +136,19 @@ test('componentManifestGenerator generates correct id, name, description and exa
243136
"description": "Primary UI component for user interaction",
244137
"error": undefined,
245138
"id": "example-button",
246-
"import": "import { Button } from "some-package";",
247-
"jsDocTags": {},
139+
"import": "import { Button } from '@design-system/components/Button';",
140+
"jsDocTags": {
141+
"import": [
142+
"import { Button } from '@design-system/components/Button';",
143+
],
144+
},
248145
"name": "Button",
249146
"path": "./src/stories/Button.stories.ts",
250147
"reactDocgen": {
251148
"actualName": "Button",
252-
"definedInFile": "/app/src/stories/Button.tsx",
253-
"description": "Primary UI component for user interaction",
149+
"definedInFile": "./src/stories/Button.tsx",
150+
"description": "Primary UI component for user interaction
151+
@import import { Button } from '@design-system/components/Button';",
254152
"displayName": "Button",
255153
"exportName": "Button",
256154
"methods": [],
@@ -347,7 +245,7 @@ test('componentManifestGenerator generates correct id, name, description and exa
347245
"description": "Description from meta and very long.",
348246
"error": undefined,
349247
"id": "example-header",
350-
"import": "import { Header } from "some-package";",
248+
"import": "import { Header } from '@design-system/components/Header';",
351249
"jsDocTags": {
352250
"import": [
353251
"import { Header } from '@design-system/components/Header';",
@@ -360,8 +258,8 @@ test('componentManifestGenerator generates correct id, name, description and exa
360258
"path": "./src/stories/Header.stories.ts",
361259
"reactDocgen": {
362260
"actualName": "",
363-
"definedInFile": "/app/src/stories/Header.tsx",
364-
"description": "",
261+
"definedInFile": "./src/stories/Header.tsx",
262+
"description": "@import import { Header } from '@design-system/components/Header';",
365263
"exportName": "default",
366264
"methods": [],
367265
"props": {
@@ -530,7 +428,7 @@ test('fall back to index title when no component name', async () => {
530428
"path": "./src/stories/Button.stories.ts",
531429
"reactDocgen": {
532430
"actualName": "Button",
533-
"definedInFile": "/app/src/stories/Button.tsx",
431+
"definedInFile": "./src/stories/Button.tsx",
534432
"description": "Primary UI component for user interaction",
535433
"displayName": "Button",
536434
"exportName": "Button",
@@ -575,7 +473,7 @@ test('component exported from other file', async () => {
575473
"path": "./src/stories/Button.stories.ts",
576474
"reactDocgen": {
577475
"actualName": "Button",
578-
"definedInFile": "/app/src/stories/Button.tsx",
476+
"definedInFile": "./src/stories/Button.tsx",
579477
"description": "Primary UI component for user interaction",
580478
"displayName": "Button",
581479
"exportName": "Button",
@@ -627,7 +525,7 @@ test('unknown expressions', async () => {
627525
"path": "./src/stories/Button.stories.ts",
628526
"reactDocgen": {
629527
"actualName": "Button",
630-
"definedInFile": "/app/src/stories/Button.tsx",
528+
"definedInFile": "./src/stories/Button.tsx",
631529
"description": "Primary UI component for user interaction",
632530
"displayName": "Button",
633531
"exportName": "Button",

code/renderers/react/src/componentManifest/generator.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ export const componentManifestGenerator: PresetPropertyFn<
3838
group && group?.length > 0 ? [group[0]] : []
3939
);
4040
const components = await Promise.all(
41-
singleEntryPerComponent.flatMap(async (entry): Promise<ReactComponentManifest> => {
41+
singleEntryPerComponent.map(async (entry): Promise<ReactComponentManifest | undefined> => {
4242
const absoluteImportPath = path.join(process.cwd(), entry.importPath);
4343
const storyFile = await readFile(absoluteImportPath, 'utf-8');
4444
const csf = loadCsf(storyFile, { makeTitle: (title) => title ?? 'No title' }).parse();
45+
46+
if (csf.meta.tags?.includes('!manifest')) {
47+
return;
48+
}
4549
let componentName = csf._meta?.component;
4650
const title = entry.title.replace(/\s+/g, '');
4751

@@ -87,10 +91,20 @@ export const componentManifestGenerator: PresetPropertyFn<
8791

8892
const stories = Object.keys(csf._stories)
8993
.map((storyName) => {
94+
const story = csf._stories[storyName];
95+
if (story.tags?.includes('!manifest')) {
96+
return;
97+
}
9098
try {
99+
const jsdocComment = extractDescription(csf._storyStatements[storyName]);
100+
const { tags = {}, description } = jsdocComment ? extractJSDocInfo(jsdocComment) : {};
101+
const finalDescription = (tags?.describe?.[0] || tags?.desc?.[0]) ?? description;
102+
91103
return {
92104
name: storyName,
93105
snippet: recast.print(getCodeSnippet(csf, storyName, componentName)).code,
106+
description: finalDescription?.trim(),
107+
summary: tags.summary?.[0],
94108
};
95109
} catch (e) {
96110
invariant(e instanceof Error);
@@ -100,7 +114,7 @@ export const componentManifestGenerator: PresetPropertyFn<
100114
};
101115
}
102116
})
103-
.filter(Boolean);
117+
.filter((it) => it != null);
104118

105119
const base = {
106120
id,

0 commit comments

Comments
 (0)