Skip to content

Commit 17e90ea

Browse files
Add react-docgen-typescript support (#167)
* refactor: use official types from react-docgen-typescript Replace custom type definitions with official ComponentDoc type from react-docgen-typescript package. Extend the official type with exportName property which is added by the manifest. This improves maintainability and alignment with the official package. * fix: use ComponentDoc from npm correctly in tests Remove excess `exportName` property and add required `methods` field to satisfy the ComponentDoc interface from react-docgen-typescript. Fix formatting with oxfmt. * move react-docgen-typescript to devDependencies Only used for type imports, consistent with react-docgen. * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent e1b559b commit 17e90ea

7 files changed

Lines changed: 396 additions & 28 deletions

File tree

packages/mcp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"devDependencies": {
4242
"@tmcp/transport-stdio": "catalog:",
4343
"react-docgen": "^8.0.2",
44+
"react-docgen-typescript": "^2.4.0",
4445
"srvx": "^0.8.16",
4546
"tinyexec": "^1.0.2"
4647
}

packages/mcp/pnpm-lock.yaml

Lines changed: 42 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/mcp/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Documentation } from 'react-docgen';
2+
import type { ComponentDoc } from 'react-docgen-typescript';
23
import * as v from 'valibot';
34

45
/**
@@ -115,6 +116,12 @@ const Doc = v.object({
115116
});
116117
export type Doc = v.InferOutput<typeof Doc>;
117118

119+
/**
120+
* Component documentation from react-docgen-typescript, extended with export name.
121+
* Matches the shape produced by Storybook's manifest generator.
122+
*/
123+
export type ComponentDocWithExportName = ComponentDoc & { exportName: string };
124+
118125
export const ComponentManifest = v.object({
119126
...BaseManifest.entries,
120127
id: v.string(),
@@ -124,6 +131,8 @@ export const ComponentManifest = v.object({
124131
stories: v.optional(v.array(Story)),
125132
// loose schema for react-docgen types, as they are pretty complex
126133
reactDocgen: v.optional(v.custom<Documentation>(() => true)),
134+
// loose schema for react-docgen-typescript types
135+
reactDocgenTypescript: v.optional(v.custom<ComponentDocWithExportName>(() => true)),
127136
docs: v.optional(v.record(v.string(), Doc)),
128137
});
129138
export type ComponentManifest = v.InferOutput<typeof ComponentManifest>;

packages/mcp/src/utils/manifest-formatter/markdown.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,158 @@ describe('MarkdownFormatter - formatComponentManifest', () => {
517517
});
518518

519519
describe('props section', () => {
520+
it('should format props from reactDocgenTypescript', () => {
521+
const manifest: ComponentManifest = {
522+
id: 'button',
523+
name: 'Button',
524+
path: 'src/components/Button.tsx',
525+
reactDocgenTypescript: {
526+
displayName: 'Button',
527+
filePath: 'src/components/Button.tsx',
528+
description: '',
529+
exportName: 'Button',
530+
methods: [],
531+
props: {
532+
variant: {
533+
name: 'variant',
534+
description: 'The visual style variant',
535+
type: { name: 'enum', raw: '"primary" | "secondary"' },
536+
defaultValue: { value: 'primary' },
537+
required: false,
538+
},
539+
disabled: {
540+
name: 'disabled',
541+
description: 'Whether the button is disabled',
542+
type: { name: 'boolean' },
543+
defaultValue: { value: 'false' },
544+
required: false,
545+
},
546+
onClick: {
547+
name: 'onClick',
548+
description: 'Click handler',
549+
type: { name: '(event: MouseEvent) => void' },
550+
defaultValue: null,
551+
required: true,
552+
},
553+
},
554+
},
555+
};
556+
557+
const result = formatComponentManifest(manifest);
558+
559+
expect(result).toMatchInlineSnapshot(`
560+
"# Button
561+
562+
ID: button
563+
564+
## Props
565+
566+
\`\`\`
567+
export type Props = {
568+
/**
569+
The visual style variant
570+
*/
571+
variant?: "primary" | "secondary" = primary;
572+
/**
573+
Whether the button is disabled
574+
*/
575+
disabled?: boolean = false;
576+
/**
577+
Click handler
578+
*/
579+
onClick: (event: MouseEvent) => void;
580+
}
581+
\`\`\`"
582+
`);
583+
});
584+
585+
it('should prefer reactDocgen over reactDocgenTypescript when both are present', () => {
586+
const manifest: ComponentManifest = {
587+
id: 'button',
588+
name: 'Button',
589+
path: 'src/components/Button.tsx',
590+
reactDocgen: {
591+
props: {
592+
label: {
593+
type: { name: 'string' },
594+
description: 'From react-docgen',
595+
},
596+
},
597+
},
598+
reactDocgenTypescript: {
599+
displayName: 'Button',
600+
filePath: 'src/components/Button.tsx',
601+
description: '',
602+
exportName: 'Button',
603+
methods: [],
604+
props: {
605+
label: {
606+
name: 'label',
607+
description: 'From react-docgen-typescript',
608+
type: { name: 'string' },
609+
defaultValue: null,
610+
required: true,
611+
},
612+
},
613+
},
614+
};
615+
616+
const result = formatComponentManifest(manifest);
617+
618+
expect(result).toContain('From react-docgen');
619+
expect(result).not.toContain('From react-docgen-typescript');
620+
});
621+
622+
it('should limit stories when reactDocgenTypescript has props', () => {
623+
const manifest: ComponentManifest = {
624+
id: 'button',
625+
name: 'Button',
626+
path: 'src/components/Button.tsx',
627+
import: 'import { Button } from "@/components";',
628+
stories: [
629+
{ name: 'Default', snippet: '<Button>Default</Button>' },
630+
{ name: 'Primary', snippet: '<Button variant="primary">Primary</Button>' },
631+
{ name: 'Secondary', snippet: '<Button variant="secondary">Secondary</Button>' },
632+
{
633+
name: 'Disabled',
634+
summary: 'Button in disabled state',
635+
snippet: '<Button disabled>Disabled</Button>',
636+
},
637+
{
638+
name: 'WithIcon',
639+
description: 'Button with an icon',
640+
snippet: '<Button icon="check">With Icon</Button>',
641+
},
642+
],
643+
reactDocgenTypescript: {
644+
displayName: 'Button',
645+
filePath: 'src/components/Button.tsx',
646+
description: '',
647+
exportName: 'Button',
648+
methods: [],
649+
props: {
650+
variant: {
651+
name: 'variant',
652+
description: '',
653+
type: { name: 'string' },
654+
defaultValue: null,
655+
required: false,
656+
},
657+
},
658+
},
659+
};
660+
661+
const result = formatComponentManifest(manifest);
662+
663+
// Should show first 3 stories in full, then remaining under "Other Stories"
664+
expect(result).toContain('### Default');
665+
expect(result).toContain('### Primary');
666+
expect(result).toContain('### Secondary');
667+
expect(result).toContain('### Other Stories');
668+
expect(result).toContain('- Disabled: Button in disabled state');
669+
expect(result).toContain('- WithIcon: Button with an icon');
670+
});
671+
520672
it('should format props with only name and type as bullet list', () => {
521673
const manifest: ComponentManifest = {
522674
id: 'button',

0 commit comments

Comments
 (0)