Skip to content

Commit cddbf34

Browse files
authored
Support name in manifest errors (#55)
* add name to manifest errors * add fixtures with errors * add changeset * improve test reports in ci * update fixtures to use stories instead of examples * more example -> story renaming
1 parent 77536a7 commit cddbf34

File tree

6 files changed

+346
-6
lines changed

6 files changed

+346
-6
lines changed

.changeset/spotty-buses-sip.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@storybook/mcp': patch
3+
---
4+
5+
Support error.name in manifests

.github/workflows/check.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ jobs:
6666
uses: ./.github/actions/setup-node-and-install
6767

6868
- name: Run tests with coverage
69-
run: pnpm --filter @storybook/mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml
69+
run: pnpm --filter @storybook/mcp test run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml
7070

7171
- name: Upload test and coverage artifact
7272
uses: actions/upload-artifact@v4
73+
if: always()
7374
with:
7475
name: test-mcp
7576
path: |
@@ -90,9 +91,10 @@ jobs:
9091
run: pnpm build --filter @storybook/mcp
9192

9293
- name: Run tests with coverage
93-
run: pnpm --filter @storybook/addon-mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml
94+
run: pnpm --filter @storybook/addon-mcp test run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml
9495

9596
- name: Upload test and coverage artifact
97+
if: always()
9698
uses: actions/upload-artifact@v4
9799
with:
98100
name: test-addon-mcp
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
{
2+
"v": 1,
3+
"components": {
4+
"success-component-with-mixed-stories": {
5+
"id": "success-component-with-mixed-stories",
6+
"path": "src/components/SuccessWithMixedStories.tsx",
7+
"name": "SuccessWithMixedStories",
8+
"description": "A component that loaded successfully but has some stories that failed to generate.",
9+
"summary": "Success component with both working and failing stories",
10+
"import": "import { SuccessWithMixedStories } from '@storybook/design-system';",
11+
"reactDocgen": {
12+
"props": {
13+
"text": {
14+
"description": "The text to display",
15+
"required": true,
16+
"tsType": { "name": "string" }
17+
},
18+
"variant": {
19+
"description": "The visual variant",
20+
"required": false,
21+
"tsType": {
22+
"name": "union",
23+
"raw": "\"primary\" | \"secondary\"",
24+
"elements": [
25+
{ "name": "literal", "value": "\"primary\"" },
26+
{ "name": "literal", "value": "\"secondary\"" }
27+
]
28+
},
29+
"defaultValue": { "value": "\"primary\"", "computed": false }
30+
}
31+
}
32+
},
33+
"stories": [
34+
{
35+
"id": "success-component-with-mixed-stories--working",
36+
"name": "Working",
37+
"description": "This story generated successfully.",
38+
"summary": "A working story",
39+
"import": "import { SuccessWithMixedStories } from '@storybook/design-system';",
40+
"snippet": "const Working = () => <SuccessWithMixedStories text=\"Hello\" />"
41+
},
42+
{
43+
"id": "success-component-with-mixed-stories--failed",
44+
"name": "Failed",
45+
"error": {
46+
"name": "SyntaxError",
47+
"message": "Unexpected token in story code. Unable to generate code snippet."
48+
}
49+
}
50+
]
51+
},
52+
"error-component-with-success-stories": {
53+
"id": "error-component-with-success-stories",
54+
"path": "src/components/ErrorWithSuccessStories.tsx",
55+
"name": "ErrorWithSuccessStories",
56+
"error": {
57+
"name": "TypeError",
58+
"message": "Failed to parse component: Cannot read property 'name' of undefined in react-docgen parser"
59+
},
60+
"stories": [
61+
{
62+
"id": "error-component-with-success-stories--basic",
63+
"name": "Basic",
64+
"description": "Even though the component parsing failed, this story's code snippet was generated.",
65+
"summary": "Basic usage story",
66+
"snippet": "const Basic = () => <ErrorWithSuccessStories>Content</ErrorWithSuccessStories>"
67+
},
68+
{
69+
"id": "error-component-with-success-stories--advanced",
70+
"name": "Advanced",
71+
"description": "Another successfully generated story despite component-level errors.",
72+
"summary": "Advanced usage story",
73+
"snippet": "const Advanced = () => (\n <ErrorWithSuccessStories disabled>\n Advanced Content\n </ErrorWithSuccessStories>\n)"
74+
}
75+
]
76+
},
77+
"error-component-with-error-stories": {
78+
"id": "error-component-with-error-stories",
79+
"path": "src/components/ErrorWithErrorStories.tsx",
80+
"name": "ErrorWithErrorStories",
81+
"error": {
82+
"name": "Error",
83+
"message": "Failed to extract component metadata: File not found or contains invalid TypeScript"
84+
},
85+
"stories": [
86+
{
87+
"id": "error-component-with-error-stories--broken-story-1",
88+
"name": "BrokenStory1",
89+
"description": "This story failed to generate.",
90+
"error": {
91+
"name": "Error",
92+
"message": "Story render function is too complex to analyze"
93+
}
94+
},
95+
{
96+
"id": "error-component-with-error-stories--broken-story-2",
97+
"name": "BrokenStory2",
98+
"description": "This story also failed to generate.",
99+
"error": {
100+
"name": "ReferenceError",
101+
"message": "Undefined variable referenced in story: missingImport"
102+
}
103+
}
104+
]
105+
},
106+
"complete-error-component": {
107+
"id": "complete-error-component",
108+
"path": "src/components/CompleteError.tsx",
109+
"name": "CompleteError",
110+
"error": {
111+
"name": "ModuleNotFoundError",
112+
"message": "Cannot find module './CompleteError' or its corresponding type declarations"
113+
}
114+
},
115+
"partial-success": {
116+
"id": "partial-success",
117+
"path": "src/components/PartialSuccess.tsx",
118+
"name": "PartialSuccess",
119+
"description": "A component where everything worked except one story.",
120+
"summary": "Mostly working component with one failing story",
121+
"import": "import { PartialSuccess } from '@storybook/design-system';",
122+
"reactDocgen": {
123+
"props": {
124+
"title": {
125+
"description": "The title text",
126+
"required": true,
127+
"tsType": { "name": "string" }
128+
},
129+
"subtitle": {
130+
"description": "Optional subtitle",
131+
"required": false,
132+
"tsType": { "name": "string" }
133+
}
134+
}
135+
},
136+
"stories": [
137+
{
138+
"id": "partial-success--default",
139+
"name": "Default",
140+
"description": "Default usage of the component.",
141+
"import": "import { PartialSuccess } from '@storybook/design-system';",
142+
"snippet": "const Default = () => <PartialSuccess title=\"Hello\" />"
143+
},
144+
{
145+
"id": "partial-success--with-subtitle",
146+
"name": "WithSubtitle",
147+
"description": "Component with both title and subtitle.",
148+
"import": "import { PartialSuccess } from '@storybook/design-system';",
149+
"snippet": "const WithSubtitle = () => <PartialSuccess title=\"Hello\" subtitle=\"World\" />"
150+
},
151+
{
152+
"id": "partial-success--complex-case",
153+
"name": "ComplexCase",
154+
"description": "A complex story that failed to generate.",
155+
"error": {
156+
"name": "Error",
157+
"message": "Story uses hooks that cannot be statically analyzed"
158+
}
159+
}
160+
]
161+
}
162+
}
163+
}

packages/mcp/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const BaseManifest = v.object({
4545
jsDocTags: v.optional(JSDocTag),
4646
error: v.optional(
4747
v.object({
48+
name: v.string(),
4849
message: v.string(),
4950
}),
5051
),

packages/mcp/src/utils/error-to-mcp-content.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@ import { errorToMCPContent, ManifestGetError } from './get-manifest.ts';
33

44
describe('errorToMCPContent', () => {
55
it('should convert ManifestGetError to MCP error content', () => {
6-
const error = new ManifestGetError(
7-
'Failed to get',
8-
'https://example.com',
9-
);
6+
const error = new ManifestGetError('Failed to get', 'https://example.com');
107

118
const result = errorToMCPContent(error);
129

packages/mcp/src/utils/format-manifest.test.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
} from './format-manifest';
66
import type { ComponentManifest, ComponentManifestMap } from '../types';
77
import fullManifestFixture from '../../fixtures/full-manifest.fixture.json' with { type: 'json' };
8+
import withErrorsFixture from '../../fixtures/with-errors.fixture.json' with { type: 'json' };
89

910
describe('formatComponentManifest', () => {
1011
it('formats all full fixtures', () => {
@@ -874,4 +875,175 @@ describe('formatComponentManifestMapToList', () => {
874875
`);
875876
});
876877
});
878+
879+
describe('with-errors fixture', () => {
880+
it('should format success component with mixed stories (only successful ones)', () => {
881+
const component =
882+
withErrorsFixture.components['success-component-with-mixed-stories'];
883+
const result = formatComponentManifest(component);
884+
expect(result).toMatchInlineSnapshot(`
885+
"<component>
886+
<id>success-component-with-mixed-stories</id>
887+
<name>SuccessWithMixedStories</name>
888+
<description>
889+
A component that loaded successfully but has some stories that failed to generate.
890+
</description>
891+
<story>
892+
<story_name>Working</story_name>
893+
<story_description>
894+
This story generated successfully.
895+
</story_description>
896+
<story_code>
897+
import { SuccessWithMixedStories } from '@storybook/design-system';
898+
899+
const Working = () => <SuccessWithMixedStories text="Hello" />
900+
</story_code>
901+
</story>
902+
<props>
903+
<prop>
904+
<prop_name>text</prop_name>
905+
<prop_description>
906+
The text to display
907+
</prop_description>
908+
<prop_type>string</prop_type>
909+
<prop_required>true</prop_required>
910+
</prop>
911+
<prop>
912+
<prop_name>variant</prop_name>
913+
<prop_description>
914+
The visual variant
915+
</prop_description>
916+
<prop_type>"primary" | "secondary"</prop_type>
917+
<prop_required>false</prop_required>
918+
<prop_default>"primary"</prop_default>
919+
</prop>
920+
</props>
921+
</component>"
922+
`);
923+
});
924+
925+
it('should format error component with success stories', () => {
926+
const component =
927+
withErrorsFixture.components['error-component-with-success-stories'];
928+
const result = formatComponentManifest(component);
929+
expect(result).toMatchInlineSnapshot(`
930+
"<component>
931+
<id>error-component-with-success-stories</id>
932+
<name>ErrorWithSuccessStories</name>
933+
<story>
934+
<story_name>Basic</story_name>
935+
<story_description>
936+
Even though the component parsing failed, this story's code snippet was generated.
937+
</story_description>
938+
<story_code>
939+
const Basic = () => <ErrorWithSuccessStories>Content</ErrorWithSuccessStories>
940+
</story_code>
941+
</story>
942+
<story>
943+
<story_name>Advanced</story_name>
944+
<story_description>
945+
Another successfully generated story despite component-level errors.
946+
</story_description>
947+
<story_code>
948+
const Advanced = () => (
949+
<ErrorWithSuccessStories disabled>
950+
Advanced Content
951+
</ErrorWithSuccessStories>
952+
)
953+
</story_code>
954+
</story>
955+
</component>"
956+
`);
957+
});
958+
959+
it('should format partial success component (skips failed story)', () => {
960+
const component = withErrorsFixture.components['partial-success'];
961+
const result = formatComponentManifest(component);
962+
expect(result).toMatchInlineSnapshot(`
963+
"<component>
964+
<id>partial-success</id>
965+
<name>PartialSuccess</name>
966+
<description>
967+
A component where everything worked except one story.
968+
</description>
969+
<story>
970+
<story_name>Default</story_name>
971+
<story_description>
972+
Default usage of the component.
973+
</story_description>
974+
<story_code>
975+
import { PartialSuccess } from '@storybook/design-system';
976+
977+
const Default = () => <PartialSuccess title="Hello" />
978+
</story_code>
979+
</story>
980+
<story>
981+
<story_name>With Subtitle</story_name>
982+
<story_description>
983+
Component with both title and subtitle.
984+
</story_description>
985+
<story_code>
986+
import { PartialSuccess } from '@storybook/design-system';
987+
988+
const WithSubtitle = () => <PartialSuccess title="Hello" subtitle="World" />
989+
</story_code>
990+
</story>
991+
<props>
992+
<prop>
993+
<prop_name>title</prop_name>
994+
<prop_description>
995+
The title text
996+
</prop_description>
997+
<prop_type>string</prop_type>
998+
<prop_required>true</prop_required>
999+
</prop>
1000+
<prop>
1001+
<prop_name>subtitle</prop_name>
1002+
<prop_description>
1003+
Optional subtitle
1004+
</prop_description>
1005+
<prop_type>string</prop_type>
1006+
<prop_required>false</prop_required>
1007+
</prop>
1008+
</props>
1009+
</component>"
1010+
`);
1011+
});
1012+
1013+
it('should format list of components with errors', () => {
1014+
const result = formatComponentManifestMapToList(
1015+
withErrorsFixture as ComponentManifestMap,
1016+
);
1017+
expect(result).toMatchInlineSnapshot(`
1018+
"<components>
1019+
<component>
1020+
<id>success-component-with-mixed-stories</id>
1021+
<name>SuccessWithMixedStories</name>
1022+
<summary>
1023+
Success component with both working and failing stories
1024+
</summary>
1025+
</component>
1026+
<component>
1027+
<id>error-component-with-success-stories</id>
1028+
<name>ErrorWithSuccessStories</name>
1029+
</component>
1030+
<component>
1031+
<id>error-component-with-error-stories</id>
1032+
<name>ErrorWithErrorStories</name>
1033+
</component>
1034+
<component>
1035+
<id>complete-error-component</id>
1036+
<name>CompleteError</name>
1037+
</component>
1038+
<component>
1039+
<id>partial-success</id>
1040+
<name>PartialSuccess</name>
1041+
<summary>
1042+
Mostly working component with one failing story
1043+
</summary>
1044+
</component>
1045+
</components>"
1046+
`);
1047+
});
1048+
});
8771049
});

0 commit comments

Comments
 (0)