Skip to content

Commit a56859f

Browse files
keidy9claude
andcommitted
fix(mcp): expand aliased enum values in react-docgen-typescript output
When a prop's type is a named alias of a string-literal union (e.g. `variant?: ButtonVariant`), `shouldExtractLiteralValuesFromEnum` makes RDT record the alias name in `type.raw` and the resolved literals in `type.value`. The serializer only read `type.raw`, so consumers got the alias name and lost the member list (`variant?: ButtonVariant` instead of `variant?: "primary" | "neutral" | ...`). Walk `type.value` for `enum`-named types and fall back to `type.raw` / `type.name` only when no members are present. Inline and aliased unions now render the same way. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 819da39 commit a56859f

3 files changed

Lines changed: 85 additions & 5 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@storybook/mcp': patch
3+
---
4+
5+
Expand aliased string-literal unions in `react-docgen-typescript` output.
6+
7+
When a prop's type is a named alias of a string-literal union (e.g. `variant?: ButtonVariant`), `shouldExtractLiteralValuesFromEnum` makes RDT record the alias name in `type.raw` and the resolved members in `type.value`. The previous serializer only read `type.raw`, so consumers received the alias name and lost the member list. The serializer now walks `type.value` for `enum`-named types and falls back to `type.raw` / `type.name` only when no members are present, so inline and aliased unions render the same way.

packages/mcp/src/utils/parse-react-docgen.test.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ describe('parseReactDocgenTypescript', () => {
348348
`);
349349
});
350350

351-
test('prefers type.raw over type.name for enums', () => {
351+
test('expands enum members for inline string-literal unions', () => {
352352
const result = parseReactDocgenTypescript({
353353
displayName: 'Button',
354354
filePath: 'src/Button.tsx',
@@ -382,6 +382,57 @@ describe('parseReactDocgenTypescript', () => {
382382
`);
383383
});
384384

385+
test('expands enum members when type.raw is a named alias (not the inline union)', () => {
386+
// With `shouldExtractLiteralValuesFromEnum: true`, RDT records `type.raw` as the
387+
// alias name (e.g. "ButtonVariant") and puts the resolved literals in `type.value`.
388+
// Without expanding `type.value`, the alias name leaks through and downstream
389+
// consumers lose the actual member list.
390+
const result = parseReactDocgenTypescript({
391+
displayName: 'Button',
392+
filePath: 'src/Button.tsx',
393+
description: '',
394+
methods: [],
395+
props: {
396+
variant: {
397+
name: 'variant',
398+
description: 'The variant',
399+
type: {
400+
name: 'enum',
401+
raw: 'ButtonVariant',
402+
value: [
403+
{ value: '"primary"' },
404+
{ value: '"neutral"' },
405+
{ value: '"danger"' },
406+
{ value: '"custom"' },
407+
],
408+
},
409+
defaultValue: { value: 'primary' },
410+
required: false,
411+
},
412+
},
413+
});
414+
expect(result.props.variant!.type).toBe('"primary" | "neutral" | "danger" | "custom"');
415+
});
416+
417+
test('falls back to type.raw when enum has no value array', () => {
418+
const result = parseReactDocgenTypescript({
419+
displayName: 'Button',
420+
filePath: 'src/Button.tsx',
421+
description: '',
422+
methods: [],
423+
props: {
424+
variant: {
425+
name: 'variant',
426+
description: '',
427+
type: { name: 'enum', raw: 'ButtonVariant' } as never,
428+
defaultValue: null,
429+
required: false,
430+
},
431+
},
432+
});
433+
expect(result.props.variant!.type).toBe('ButtonVariant');
434+
});
435+
385436
test('falls back to type.name when type.raw is not present', () => {
386437
const result = parseReactDocgenTypescript({
387438
displayName: 'Callback',

packages/mcp/src/utils/parse-react-docgen.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,34 @@ export const parseReactDocgen = (reactDocgen: Documentation): ParsedDocgen => {
9292
};
9393
};
9494

95+
/**
96+
* Serialize a react-docgen-typescript prop type into a TypeScript-like string.
97+
*
98+
* For enum types (which RDT uses for both string-literal unions and TS enums when
99+
* `shouldExtractLiteralValuesFromEnum` is enabled), `type.raw` only contains the
100+
* literal members when the union is written inline on the prop. When the prop
101+
* references a named alias (e.g. `variant?: ButtonVariant`), `type.raw` is just
102+
* the alias name, while the resolved literal members live in `type.value`.
103+
* Walking `type.value` ensures aliased unions are expanded the same as inline ones.
104+
*/
105+
const serializeRdtType = (
106+
type: ComponentDoc['props'][string]['type'] | undefined,
107+
): string | undefined => {
108+
if (!type) return undefined;
109+
const value = (type as { value?: unknown }).value;
110+
if (type.name === 'enum' && Array.isArray(value)) {
111+
const members = value
112+
.map((v) => (v && typeof v === 'object' && 'value' in v ? (v as { value: unknown }).value : undefined))
113+
.filter((v): v is string => typeof v === 'string');
114+
if (members.length > 0) return members.join(' | ');
115+
}
116+
return type.raw ?? type.name;
117+
};
118+
95119
/**
96120
* Parse react-docgen-typescript output into the same simplified ParsedDocgen format.
97121
* RDT uses flat type strings (prop.type.name / prop.type.raw) instead of react-docgen's
98-
* nested tsType structure, so no serialization is needed.
122+
* nested tsType structure.
99123
*/
100124
const parseComponentDocLike = (componentDoc: ComponentDocLike): ParsedDocgen => {
101125
const props = componentDoc.props ?? {};
@@ -105,9 +129,7 @@ const parseComponentDocLike = (componentDoc: ComponentDocLike): ParsedDocgen =>
105129
propName,
106130
{
107131
description: prop.description || undefined,
108-
// RDT uses prop.type.name as a flat string (e.g. "() => void", "{ id: string }")
109-
// For enums, prefer prop.type.raw which has the full union
110-
type: prop.type?.raw ?? prop.type?.name,
132+
type: serializeRdtType(prop.type),
111133
defaultValue: prop.defaultValue?.value,
112134
required: prop.required,
113135
},

0 commit comments

Comments
 (0)