diff --git a/.changeset/spotty-buses-sip.md b/.changeset/spotty-buses-sip.md new file mode 100644 index 0000000..ca240a3 --- /dev/null +++ b/.changeset/spotty-buses-sip.md @@ -0,0 +1,5 @@ +--- +'@storybook/mcp': patch +--- + +Support error.name in manifests diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index cc49e2f..f511fed 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -66,10 +66,11 @@ jobs: uses: ./.github/actions/setup-node-and-install - name: Run tests with coverage - run: pnpm --filter @storybook/mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml + run: pnpm --filter @storybook/mcp test run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml - name: Upload test and coverage artifact uses: actions/upload-artifact@v4 + if: always() with: name: test-mcp path: | @@ -90,9 +91,10 @@ jobs: run: pnpm build --filter @storybook/mcp - name: Run tests with coverage - run: pnpm --filter @storybook/addon-mcp test run --coverage --reporter=junit --outputFile=test-report.junit.xml + run: pnpm --filter @storybook/addon-mcp test run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml - name: Upload test and coverage artifact + if: always() uses: actions/upload-artifact@v4 with: name: test-addon-mcp diff --git a/packages/mcp/fixtures/with-errors.fixture.json b/packages/mcp/fixtures/with-errors.fixture.json new file mode 100644 index 0000000..b1a8da8 --- /dev/null +++ b/packages/mcp/fixtures/with-errors.fixture.json @@ -0,0 +1,163 @@ +{ + "v": 1, + "components": { + "success-component-with-mixed-stories": { + "id": "success-component-with-mixed-stories", + "path": "src/components/SuccessWithMixedStories.tsx", + "name": "SuccessWithMixedStories", + "description": "A component that loaded successfully but has some stories that failed to generate.", + "summary": "Success component with both working and failing stories", + "import": "import { SuccessWithMixedStories } from '@storybook/design-system';", + "reactDocgen": { + "props": { + "text": { + "description": "The text to display", + "required": true, + "tsType": { "name": "string" } + }, + "variant": { + "description": "The visual variant", + "required": false, + "tsType": { + "name": "union", + "raw": "\"primary\" | \"secondary\"", + "elements": [ + { "name": "literal", "value": "\"primary\"" }, + { "name": "literal", "value": "\"secondary\"" } + ] + }, + "defaultValue": { "value": "\"primary\"", "computed": false } + } + } + }, + "stories": [ + { + "id": "success-component-with-mixed-stories--working", + "name": "Working", + "description": "This story generated successfully.", + "summary": "A working story", + "import": "import { SuccessWithMixedStories } from '@storybook/design-system';", + "snippet": "const Working = () => " + }, + { + "id": "success-component-with-mixed-stories--failed", + "name": "Failed", + "error": { + "name": "SyntaxError", + "message": "Unexpected token in story code. Unable to generate code snippet." + } + } + ] + }, + "error-component-with-success-stories": { + "id": "error-component-with-success-stories", + "path": "src/components/ErrorWithSuccessStories.tsx", + "name": "ErrorWithSuccessStories", + "error": { + "name": "TypeError", + "message": "Failed to parse component: Cannot read property 'name' of undefined in react-docgen parser" + }, + "stories": [ + { + "id": "error-component-with-success-stories--basic", + "name": "Basic", + "description": "Even though the component parsing failed, this story's code snippet was generated.", + "summary": "Basic usage story", + "snippet": "const Basic = () => Content" + }, + { + "id": "error-component-with-success-stories--advanced", + "name": "Advanced", + "description": "Another successfully generated story despite component-level errors.", + "summary": "Advanced usage story", + "snippet": "const Advanced = () => (\n \n Advanced Content\n \n)" + } + ] + }, + "error-component-with-error-stories": { + "id": "error-component-with-error-stories", + "path": "src/components/ErrorWithErrorStories.tsx", + "name": "ErrorWithErrorStories", + "error": { + "name": "Error", + "message": "Failed to extract component metadata: File not found or contains invalid TypeScript" + }, + "stories": [ + { + "id": "error-component-with-error-stories--broken-story-1", + "name": "BrokenStory1", + "description": "This story failed to generate.", + "error": { + "name": "Error", + "message": "Story render function is too complex to analyze" + } + }, + { + "id": "error-component-with-error-stories--broken-story-2", + "name": "BrokenStory2", + "description": "This story also failed to generate.", + "error": { + "name": "ReferenceError", + "message": "Undefined variable referenced in story: missingImport" + } + } + ] + }, + "complete-error-component": { + "id": "complete-error-component", + "path": "src/components/CompleteError.tsx", + "name": "CompleteError", + "error": { + "name": "ModuleNotFoundError", + "message": "Cannot find module './CompleteError' or its corresponding type declarations" + } + }, + "partial-success": { + "id": "partial-success", + "path": "src/components/PartialSuccess.tsx", + "name": "PartialSuccess", + "description": "A component where everything worked except one story.", + "summary": "Mostly working component with one failing story", + "import": "import { PartialSuccess } from '@storybook/design-system';", + "reactDocgen": { + "props": { + "title": { + "description": "The title text", + "required": true, + "tsType": { "name": "string" } + }, + "subtitle": { + "description": "Optional subtitle", + "required": false, + "tsType": { "name": "string" } + } + } + }, + "stories": [ + { + "id": "partial-success--default", + "name": "Default", + "description": "Default usage of the component.", + "import": "import { PartialSuccess } from '@storybook/design-system';", + "snippet": "const Default = () => " + }, + { + "id": "partial-success--with-subtitle", + "name": "WithSubtitle", + "description": "Component with both title and subtitle.", + "import": "import { PartialSuccess } from '@storybook/design-system';", + "snippet": "const WithSubtitle = () => " + }, + { + "id": "partial-success--complex-case", + "name": "ComplexCase", + "description": "A complex story that failed to generate.", + "error": { + "name": "Error", + "message": "Story uses hooks that cannot be statically analyzed" + } + } + ] + } + } +} diff --git a/packages/mcp/src/types.ts b/packages/mcp/src/types.ts index d13998d..c36e2f6 100644 --- a/packages/mcp/src/types.ts +++ b/packages/mcp/src/types.ts @@ -45,6 +45,7 @@ const BaseManifest = v.object({ jsDocTags: v.optional(JSDocTag), error: v.optional( v.object({ + name: v.string(), message: v.string(), }), ), diff --git a/packages/mcp/src/utils/error-to-mcp-content.test.ts b/packages/mcp/src/utils/error-to-mcp-content.test.ts index 4e190df..9d8bf7d 100644 --- a/packages/mcp/src/utils/error-to-mcp-content.test.ts +++ b/packages/mcp/src/utils/error-to-mcp-content.test.ts @@ -3,10 +3,7 @@ import { errorToMCPContent, ManifestGetError } from './get-manifest.ts'; describe('errorToMCPContent', () => { it('should convert ManifestGetError to MCP error content', () => { - const error = new ManifestGetError( - 'Failed to get', - 'https://example.com', - ); + const error = new ManifestGetError('Failed to get', 'https://example.com'); const result = errorToMCPContent(error); diff --git a/packages/mcp/src/utils/format-manifest.test.ts b/packages/mcp/src/utils/format-manifest.test.ts index 448d636..26319de 100644 --- a/packages/mcp/src/utils/format-manifest.test.ts +++ b/packages/mcp/src/utils/format-manifest.test.ts @@ -5,6 +5,7 @@ import { } from './format-manifest'; import type { ComponentManifest, ComponentManifestMap } from '../types'; import fullManifestFixture from '../../fixtures/full-manifest.fixture.json' with { type: 'json' }; +import withErrorsFixture from '../../fixtures/with-errors.fixture.json' with { type: 'json' }; describe('formatComponentManifest', () => { it('formats all full fixtures', () => { @@ -874,4 +875,175 @@ describe('formatComponentManifestMapToList', () => { `); }); }); + + describe('with-errors fixture', () => { + it('should format success component with mixed stories (only successful ones)', () => { + const component = + withErrorsFixture.components['success-component-with-mixed-stories']; + const result = formatComponentManifest(component); + expect(result).toMatchInlineSnapshot(` + " + success-component-with-mixed-stories + SuccessWithMixedStories + + A component that loaded successfully but has some stories that failed to generate. + + + Working + + This story generated successfully. + + + import { SuccessWithMixedStories } from '@storybook/design-system'; + + const Working = () => + + + + + text + + The text to display + + string + true + + + variant + + The visual variant + + "primary" | "secondary" + false + "primary" + + + " + `); + }); + + it('should format error component with success stories', () => { + const component = + withErrorsFixture.components['error-component-with-success-stories']; + const result = formatComponentManifest(component); + expect(result).toMatchInlineSnapshot(` + " + error-component-with-success-stories + ErrorWithSuccessStories + + Basic + + Even though the component parsing failed, this story's code snippet was generated. + + + const Basic = () => Content + + + + Advanced + + Another successfully generated story despite component-level errors. + + + const Advanced = () => ( + + Advanced Content + + ) + + + " + `); + }); + + it('should format partial success component (skips failed story)', () => { + const component = withErrorsFixture.components['partial-success']; + const result = formatComponentManifest(component); + expect(result).toMatchInlineSnapshot(` + " + partial-success + PartialSuccess + + A component where everything worked except one story. + + + Default + + Default usage of the component. + + + import { PartialSuccess } from '@storybook/design-system'; + + const Default = () => + + + + With Subtitle + + Component with both title and subtitle. + + + import { PartialSuccess } from '@storybook/design-system'; + + const WithSubtitle = () => + + + + + title + + The title text + + string + true + + + subtitle + + Optional subtitle + + string + false + + + " + `); + }); + + it('should format list of components with errors', () => { + const result = formatComponentManifestMapToList( + withErrorsFixture as ComponentManifestMap, + ); + expect(result).toMatchInlineSnapshot(` + " + + success-component-with-mixed-stories + SuccessWithMixedStories + + Success component with both working and failing stories + + + + error-component-with-success-stories + ErrorWithSuccessStories + + + error-component-with-error-stories + ErrorWithErrorStories + + + complete-error-component + CompleteError + + + partial-success + PartialSuccess + + Mostly working component with one failing story + + + " + `); + }); + }); });