Skip to content

Commit ca11722

Browse files
authored
Merge pull request #33914 from storybookjs/kasper/react-prop-extraction-lsp
React: Add component metadata extraction via Volar-style LanguageService
2 parents f95a174 + 3e60442 commit ca11722

36 files changed

+7937
-562
lines changed

code/core/src/core-server/utils/manifests/manifests.test.ts

Lines changed: 58 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { logger } from 'storybook/internal/node-logger';
44
import type { ComponentsManifest, Manifests, Presets, StoryIndex } from 'storybook/internal/types';
55

66
import { vol } from 'memfs';
7-
import type { Polka, Request, Response } from 'polka';
7+
import type { Polka } from 'polka';
88

99
import { Tag } from '../../../shared/constants/tags';
1010
import { registerManifests, writeManifests } from './manifests';
@@ -18,7 +18,20 @@ vi.mock('storybook/internal/node-logger');
1818

1919
describe('manifests', () => {
2020
let mockGenerator: { getIndex: ReturnType<typeof vi.fn> };
21-
let mockManifests: Manifests;
21+
let mockManifests: Manifests | null;
22+
23+
type RouteHandler = (req: { params?: { name?: string } }, res: MockResponse) => Promise<void>;
24+
type MockResponse = {
25+
setHeader: ReturnType<typeof vi.fn>;
26+
end: ReturnType<typeof vi.fn>;
27+
statusCode?: number | undefined;
28+
};
29+
30+
const createResponse = (): MockResponse => ({
31+
setHeader: vi.fn(),
32+
end: vi.fn(),
33+
statusCode: undefined,
34+
});
2235

2336
const setupMockPresets = () => {
2437
mockGenerator = {
@@ -34,12 +47,12 @@ describe('manifests', () => {
3447
case 'storyIndexGenerator':
3548
return Promise.resolve(mockGenerator);
3649
case 'experimental_manifests':
37-
return Promise.resolve(mockManifests);
50+
return Promise.resolve(mockManifests ?? undefined);
3851
default:
3952
return Promise.resolve(undefined);
4053
}
4154
}),
42-
} as any as Presets;
55+
} satisfies Presets;
4356
};
4457

4558
beforeEach(() => {
@@ -193,12 +206,18 @@ describe('manifests', () => {
193206
);
194207

195208
// Get the specific apply call to the experimental_manifests preset
196-
const manifestsPresetCall = (mockPresets.apply as any).mock.calls.find(
197-
(call: any) => call[0] === 'experimental_manifests'
198-
);
209+
const manifestsPresetCall = vi
210+
.mocked(mockPresets.apply)
211+
.mock.calls.find((call) => call[0] === 'experimental_manifests');
212+
213+
expect(manifestsPresetCall).toBeDefined();
214+
const manifestEntriesArg =
215+
(manifestsPresetCall?.[2] as { manifestEntries?: Array<{ id: string }> })
216+
?.manifestEntries ?? [];
217+
199218
// Should include both story and docs entries with manifest tag
200-
expect(manifestsPresetCall[2].manifestEntries).toHaveLength(2);
201-
const entryIds = manifestsPresetCall[2].manifestEntries.map((entry: any) => entry.id);
219+
expect(manifestEntriesArg).toHaveLength(2);
220+
const entryIds = manifestEntriesArg.map((entry) => entry.id);
202221
expect(entryIds).toContain('story-with-manifest');
203222
expect(entryIds).toContain('docs');
204223
// Should NOT include story without manifest tag
@@ -213,7 +232,7 @@ describe('manifests', () => {
213232

214233
beforeEach(() => {
215234
mockGet = vi.fn();
216-
mockApp = { get: mockGet } as any;
235+
mockApp = { get: mockGet } as unknown as Polka;
217236
mockPresets = setupMockPresets();
218237
});
219238

@@ -235,12 +254,9 @@ describe('manifests', () => {
235254

236255
registerManifests({ app: mockApp, presets: mockPresets });
237256

238-
const handler = mockGet.mock.calls[0][1];
239-
const req = { params: { name: 'custom' } } as any as Request;
240-
const res = {
241-
setHeader: vi.fn(),
242-
end: vi.fn(),
243-
} as any as Response;
257+
const handler = mockGet.mock.calls[0][1] as RouteHandler;
258+
const req = { params: { name: 'custom' } };
259+
const res = createResponse();
244260

245261
await handler(req, res);
246262

@@ -256,13 +272,9 @@ describe('manifests', () => {
256272

257273
registerManifests({ app: mockApp, presets: mockPresets });
258274

259-
const handler = mockGet.mock.calls[0][1];
275+
const handler = mockGet.mock.calls[0][1] as RouteHandler;
260276
const req = { params: { name: 'nonexistent' } };
261-
const res = {
262-
setHeader: vi.fn(),
263-
end: vi.fn(),
264-
statusCode: undefined as number | undefined,
265-
};
277+
const res = createResponse();
266278

267279
await handler(req, res);
268280

@@ -275,13 +287,9 @@ describe('manifests', () => {
275287

276288
registerManifests({ app: mockApp, presets: mockPresets });
277289

278-
const handler = mockGet.mock.calls[0][1];
290+
const handler = mockGet.mock.calls[0][1] as RouteHandler;
279291
const req = { params: { name: 'any' } };
280-
const res = {
281-
setHeader: vi.fn(),
282-
end: vi.fn(),
283-
statusCode: undefined as number | undefined,
284-
};
292+
const res = createResponse();
285293

286294
await handler(req, res);
287295

@@ -295,13 +303,9 @@ describe('manifests', () => {
295303

296304
registerManifests({ app: mockApp, presets: mockPresets });
297305

298-
const handler = mockGet.mock.calls[0][1];
299-
const req = { params: { name: 'custom' } } as any as Request;
300-
const res = {
301-
setHeader: vi.fn(),
302-
end: vi.fn(),
303-
statusCode: undefined as number | undefined,
304-
} as any as Response;
306+
const handler = mockGet.mock.calls[0][1] as RouteHandler;
307+
const req = { params: { name: 'custom' } };
308+
const res = createResponse();
305309

306310
await handler(req, res);
307311

@@ -316,13 +320,9 @@ describe('manifests', () => {
316320

317321
registerManifests({ app: mockApp, presets: mockPresets });
318322

319-
const handler = mockGet.mock.calls[0][1];
320-
const req = { params: { name: 'custom' } } as any as Request;
321-
const res = {
322-
setHeader: vi.fn(),
323-
end: vi.fn(),
324-
statusCode: undefined as number | undefined,
325-
} as any as Response;
323+
const handler = mockGet.mock.calls[0][1] as RouteHandler;
324+
const req = { params: { name: 'custom' } };
325+
const res = createResponse();
326326

327327
await handler(req, res);
328328

@@ -332,17 +332,13 @@ describe('manifests', () => {
332332
});
333333

334334
it('should handle when presets.apply returns null/undefined', async () => {
335-
mockManifests = null as any;
335+
mockManifests = null;
336336

337337
registerManifests({ app: mockApp, presets: mockPresets });
338338

339-
const handler = mockGet.mock.calls[0][1];
340-
const req = { params: { name: 'custom' } } as any as Request;
341-
const res = {
342-
setHeader: vi.fn(),
343-
end: vi.fn(),
344-
statusCode: undefined as number | undefined,
345-
} as any as Response;
339+
const handler = mockGet.mock.calls[0][1] as RouteHandler;
340+
const req = { params: { name: 'custom' } };
341+
const res = createResponse();
346342

347343
await handler(req, res);
348344

@@ -371,18 +367,15 @@ describe('manifests', () => {
371367

372368
registerManifests({ app: mockApp, presets: mockPresets });
373369

374-
const handler = mockGet.mock.calls[1][1];
375-
const req = {} as any as Request;
376-
const res = {
377-
setHeader: vi.fn(),
378-
end: vi.fn(),
379-
} as any as Response;
370+
const handler = mockGet.mock.calls[1][1] as RouteHandler;
371+
const req = {};
372+
const res = createResponse();
380373

381374
await handler(req, res);
382375

383376
expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'text/html; charset=utf-8');
384377
expect(res.end).toHaveBeenCalled();
385-
const html = (res.end as any).mock.calls[0][0];
378+
const html = res.end.mock.calls[0]?.[0];
386379
expect(html).toContain('<!doctype html>');
387380
expect(html).toContain('Manifest Debugger');
388381
expect(res.statusCode).toBeUndefined();
@@ -395,13 +388,9 @@ describe('manifests', () => {
395388

396389
registerManifests({ app: mockApp, presets: mockPresets });
397390

398-
const handler = mockGet.mock.calls[1][1];
391+
const handler = mockGet.mock.calls[1][1] as RouteHandler;
399392
const req = {};
400-
const res = {
401-
setHeader: vi.fn(),
402-
end: vi.fn(),
403-
statusCode: undefined as number | undefined,
404-
};
393+
const res = createResponse();
405394

406395
await handler(req, res);
407396

@@ -417,13 +406,9 @@ describe('manifests', () => {
417406

418407
registerManifests({ app: mockApp, presets: mockPresets });
419408

420-
const handler = mockGet.mock.calls[1][1];
409+
const handler = mockGet.mock.calls[1][1] as RouteHandler;
421410
const req = {};
422-
const res = {
423-
setHeader: vi.fn(),
424-
end: vi.fn(),
425-
statusCode: undefined as number | undefined,
426-
};
411+
const res = createResponse();
427412

428413
await handler(req, res);
429414

@@ -440,13 +425,9 @@ describe('manifests', () => {
440425

441426
registerManifests({ app: mockApp, presets: mockPresets });
442427

443-
const handler = mockGet.mock.calls[1][1];
428+
const handler = mockGet.mock.calls[1][1] as RouteHandler;
444429
const req = {};
445-
const res = {
446-
setHeader: vi.fn(),
447-
end: vi.fn(),
448-
statusCode: undefined as number | undefined,
449-
};
430+
const res = createResponse();
450431

451432
await handler(req, res);
452433

@@ -463,13 +444,9 @@ describe('manifests', () => {
463444

464445
registerManifests({ app: mockApp, presets: mockPresets });
465446

466-
const handler = mockGet.mock.calls[1][1];
447+
const handler = mockGet.mock.calls[1][1] as RouteHandler;
467448
const req = {};
468-
const res = {
469-
setHeader: vi.fn(),
470-
end: vi.fn(),
471-
statusCode: undefined as number | undefined,
472-
};
449+
const res = createResponse();
473450

474451
await handler(req, res);
475452

code/core/src/core-server/utils/manifests/manifests.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { mkdir, writeFile } from 'node:fs/promises';
22

33
import { logger } from 'storybook/internal/node-logger';
4-
import type { ComponentsManifest, Manifests, Presets } from 'storybook/internal/types';
4+
import type { Manifests, Presets } from 'storybook/internal/types';
55

66
import { join } from 'pathe';
77
import type { Polka } from 'polka';
@@ -10,7 +10,11 @@ import invariant from 'tiny-invariant';
1010
import { Tag } from '../../../shared/constants/tags';
1111
import { type DocsManifest, renderComponentsManifest } from './render-components-manifest';
1212

13-
async function getManifests(presets: Presets) {
13+
function isDocsManifest(manifest: unknown): manifest is DocsManifest {
14+
return typeof manifest === 'object' && manifest !== null && 'docs' in manifest;
15+
}
16+
17+
async function getManifests(presets: Presets, { watch }: { watch?: boolean } = {}) {
1418
const generator = await presets.apply('storyIndexGenerator');
1519
invariant(generator, 'storyIndexGenerator must be configured');
1620
const index = await generator.getIndex();
@@ -21,13 +25,16 @@ async function getManifests(presets: Presets) {
2125
return (
2226
(await presets.apply<Manifests>('experimental_manifests', undefined, {
2327
manifestEntries,
28+
watch,
2429
})) ?? {}
2530
);
2631
}
2732

2833
export async function writeManifests(outputDir: string, presets: Presets) {
2934
try {
3035
const manifests = await getManifests(presets);
36+
const docsManifest = isDocsManifest(manifests.docs) ? manifests.docs : undefined;
37+
3138
if (Object.keys(manifests).length === 0) {
3239
return;
3340
}
@@ -40,10 +47,7 @@ export async function writeManifests(outputDir: string, presets: Presets) {
4047
if ('components' in manifests || 'docs' in manifests) {
4148
await writeFile(
4249
join(outputDir, 'manifests', 'components.html'),
43-
renderComponentsManifest(
44-
manifests.components as ComponentsManifest | undefined,
45-
manifests.docs as DocsManifest | undefined
46-
)
50+
renderComponentsManifest(manifests.components, docsManifest)
4751
);
4852
}
4953
} catch (e) {
@@ -55,7 +59,7 @@ export async function writeManifests(outputDir: string, presets: Presets) {
5559
export function registerManifests({ app, presets }: { app: Polka; presets: Presets }) {
5660
app.get('/manifests/:name.json', async (req, res) => {
5761
try {
58-
const manifests = await getManifests(presets);
62+
const manifests = await getManifests(presets, { watch: true });
5963
const manifest = manifests[req.params.name];
6064

6165
if (manifest) {
@@ -74,9 +78,9 @@ export function registerManifests({ app, presets }: { app: Polka; presets: Prese
7478

7579
app.get('/manifests/components.html', async (req, res) => {
7680
try {
77-
const manifests = await getManifests(presets);
81+
const manifests = await getManifests(presets, { watch: true });
7882
const componentsManifest = manifests.components;
79-
const docsManifest = manifests.docs as DocsManifest | undefined;
83+
const docsManifest = isDocsManifest(manifests.docs) ? manifests.docs : undefined;
8084

8185
if (!componentsManifest && !docsManifest) {
8286
res.statusCode = 404;

0 commit comments

Comments
 (0)