Skip to content

Commit dc1ee3a

Browse files
authored
Merge pull request #37 from storybookjs/kasper/parse-react-docgen
Parse React docgen into a simpler structure for manifest formatting
2 parents 3838129 + 067e8c0 commit dc1ee3a

File tree

4 files changed

+524
-115
lines changed

4 files changed

+524
-115
lines changed

packages/mcp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@tsconfig/node24": "^24.0.1",
4444
"@types/node": "catalog:",
4545
"@vitest/coverage-v8": "catalog:",
46+
"react-docgen": "^8.0.2",
4647
"srvx": "^0.8.16",
4748
"tsdown": "catalog:",
4849
"typescript": "catalog:",
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
import type { Documentation } from 'react-docgen';
2+
import { describe, expect, test } from 'vitest';
3+
import { parseReactDocgen } from './parse-react-docgen';
4+
5+
describe('parseReactDocgen', () => {
6+
test('prefers raw over computed for unions (and copies default/required)', () => {
7+
const result = parseReactDocgen({
8+
props: {
9+
size: {
10+
description: 'Visual size',
11+
required: false,
12+
defaultValue: { value: '"md"', computed: false },
13+
tsType: {
14+
name: 'union',
15+
raw: '"sm" | "md" | "lg"',
16+
elements: [
17+
{ name: 'literal', value: '"sm"' },
18+
{ name: 'literal', value: '"md"' },
19+
{ name: 'literal', value: '"lg"' },
20+
],
21+
},
22+
},
23+
},
24+
});
25+
expect(result).toMatchInlineSnapshot(`
26+
{
27+
"props": {
28+
"size": {
29+
"defaultValue": "\"md\"",
30+
"description": "Visual size",
31+
"required": false,
32+
"type": "\"sm\" | \"md\" | \"lg\"",
33+
},
34+
},
35+
}
36+
`);
37+
});
38+
39+
test('serializes union when raw is missing', () => {
40+
const result = parseReactDocgen({
41+
props: {
42+
tone: {
43+
description: 'Semantic tone',
44+
required: false,
45+
tsType: {
46+
name: 'union',
47+
elements: [
48+
{ name: 'literal', value: '"primary"' },
49+
{ name: 'literal', value: '"secondary"' },
50+
],
51+
},
52+
},
53+
},
54+
});
55+
expect(result).toMatchInlineSnapshot(`
56+
{
57+
"props": {
58+
"tone": {
59+
"defaultValue": undefined,
60+
"description": "Semantic tone",
61+
"required": false,
62+
"type": "\"primary\" | \"secondary\"",
63+
},
64+
},
65+
}
66+
`);
67+
});
68+
69+
test('serializes intersection', () => {
70+
const result = parseReactDocgen({
71+
props: {
72+
options: {
73+
tsType: {
74+
name: 'intersection',
75+
elements: [
76+
{
77+
name: 'Record',
78+
elements: [{ name: 'string' }, { name: 'number' }],
79+
},
80+
{
81+
name: 'signature',
82+
type: 'object',
83+
signature: {
84+
properties: [
85+
{ key: 'a', value: { name: 'string', required: true } },
86+
],
87+
},
88+
},
89+
],
90+
},
91+
},
92+
},
93+
});
94+
expect(result).toMatchInlineSnapshot(`
95+
{
96+
"props": {
97+
"options": {
98+
"defaultValue": undefined,
99+
"description": undefined,
100+
"required": undefined,
101+
"type": "Record<string, number> & { a: string }",
102+
},
103+
},
104+
}
105+
`);
106+
});
107+
108+
test('serializes Array fallback as T[]', () => {
109+
const result = parseReactDocgen({
110+
props: {
111+
tags: {
112+
tsType: { name: 'Array', elements: [{ name: 'string' }] },
113+
defaultValue: { value: '[]', computed: false },
114+
},
115+
},
116+
});
117+
expect(result).toMatchInlineSnapshot(`
118+
{
119+
"props": {
120+
"tags": {
121+
"defaultValue": "[]",
122+
"description": undefined,
123+
"required": undefined,
124+
"type": "string[]",
125+
},
126+
},
127+
}
128+
`);
129+
});
130+
131+
test('serializes tuple', () => {
132+
const result = parseReactDocgen({
133+
props: {
134+
anchor: {
135+
tsType: {
136+
name: 'tuple',
137+
elements: [{ name: 'literal', value: '"x"' }, { name: 'number' }],
138+
},
139+
},
140+
},
141+
});
142+
expect(result).toMatchInlineSnapshot(`
143+
{
144+
"props": {
145+
"anchor": {
146+
"defaultValue": undefined,
147+
"description": undefined,
148+
"required": undefined,
149+
"type": "[\"x\", number]",
150+
},
151+
},
152+
}
153+
`);
154+
});
155+
156+
test('serializes literal', () => {
157+
const result = parseReactDocgen({
158+
props: {
159+
variant: {
160+
tsType: { name: 'literal', value: '"solid"' },
161+
},
162+
},
163+
});
164+
expect(result).toMatchInlineSnapshot(`
165+
{
166+
"props": {
167+
"variant": {
168+
"defaultValue": undefined,
169+
"description": undefined,
170+
"required": undefined,
171+
"type": "\"solid\"",
172+
},
173+
},
174+
}
175+
`);
176+
});
177+
178+
test('serializes function signature', () => {
179+
const result = parseReactDocgen({
180+
props: {
181+
onClick: {
182+
description: 'Click handler',
183+
tsType: {
184+
name: 'signature',
185+
type: 'function',
186+
signature: {
187+
arguments: [{ name: 'ev', type: { name: 'MouseEvent' } }],
188+
return: { name: 'void' },
189+
},
190+
},
191+
},
192+
},
193+
});
194+
expect(result).toMatchInlineSnapshot(`
195+
{
196+
"props": {
197+
"onClick": {
198+
"defaultValue": undefined,
199+
"description": "Click handler",
200+
"required": undefined,
201+
"type": "(ev: MouseEvent) => void",
202+
},
203+
},
204+
}
205+
`);
206+
});
207+
208+
test('serializes object signature with required and optional properties', () => {
209+
const result = parseReactDocgen({
210+
props: {
211+
asKind: {
212+
tsType: {
213+
name: 'signature',
214+
type: 'object',
215+
signature: {
216+
properties: [
217+
{
218+
key: 'kind',
219+
value: { name: 'literal', value: '"button"', required: true },
220+
},
221+
{
222+
key: 'type',
223+
value: {
224+
name: 'union',
225+
raw: '"button" | "submit" | "reset"',
226+
required: false,
227+
},
228+
},
229+
],
230+
},
231+
},
232+
defaultValue: {
233+
value: '{ kind: "button", type: "button" }',
234+
computed: false,
235+
},
236+
},
237+
},
238+
});
239+
expect(result).toMatchInlineSnapshot(`
240+
{
241+
"props": {
242+
"asKind": {
243+
"defaultValue": "{ kind: \"button\", type: \"button\" }",
244+
"description": undefined,
245+
"required": undefined,
246+
"type": "{ kind: \"button\"; type?: \"button\" | \"submit\" | \"reset\" }",
247+
},
248+
},
249+
}
250+
`);
251+
});
252+
253+
test('generic type serialization and bare names', () => {
254+
const result = parseReactDocgen({
255+
props: {
256+
items: {
257+
tsType: {
258+
name: 'Array',
259+
elements: [{ name: 'Item', elements: [{ name: 'TMeta' }] }],
260+
},
261+
},
262+
children: { tsType: { name: 'ReactNode' } },
263+
},
264+
});
265+
expect(result).toMatchInlineSnapshot(`
266+
{
267+
"props": {
268+
"children": {
269+
"defaultValue": undefined,
270+
"description": undefined,
271+
"required": undefined,
272+
"type": "ReactNode",
273+
},
274+
"items": {
275+
"defaultValue": undefined,
276+
"description": undefined,
277+
"required": undefined,
278+
"type": "Item<TMeta>[]",
279+
},
280+
},
281+
}
282+
`);
283+
});
284+
285+
test('handles undefined tsType and defaultValue null', () => {
286+
const result = parseReactDocgen({
287+
props: {
288+
icon: {
289+
tsType: undefined,
290+
defaultValue: { value: null, computed: false },
291+
required: true,
292+
},
293+
},
294+
});
295+
expect(result).toMatchInlineSnapshot(`
296+
{
297+
"props": {
298+
"icon": {
299+
"defaultValue": null,
300+
"description": undefined,
301+
"required": true,
302+
"type": undefined,
303+
},
304+
},
305+
}
306+
`);
307+
});
308+
});

0 commit comments

Comments
 (0)