Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 131 additions & 33 deletions code/addons/docs/src/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@ beforeEach(() => {
vi.spyOn(process, 'cwd').mockReturnValue('/app');
vol.fromJSON(
{
'./Example.mdx': '# Example\n\nThis is example documentation.',
'./Standalone.mdx': '# Standalone\n\nThis is standalone documentation.',
'./Example.mdx': `import { Meta } from '@storybook/addon-docs/blocks';

<Meta summary="An example documentation page" />

# Example

This is example documentation.`,
'./Standalone.mdx': `import { Meta } from '@storybook/addon-docs/blocks';

<Meta summary="A standalone documentation page" />

# Standalone

This is standalone documentation.`,
},
'/app'
);
Expand Down Expand Up @@ -152,15 +164,16 @@ describe('experimental_manifests', () => {

expect(result).toHaveProperty('components');
expect(result).not.toHaveProperty('docs');
expect(result.components?.components.example.docs).toEqual({
'example--docs': {
id: 'example--docs',
name: 'docs',
path: './Example.mdx',
title: 'Example',
content: '# Example\n\nThis is example documentation.',
},
expect(result.components?.components.example.docs?.['example--docs']).toMatchObject({
id: 'example--docs',
name: 'docs',
path: './Example.mdx',
title: 'Example',
summary: 'An example documentation page',
});
expect(result.components?.components.example.docs?.['example--docs'].content).toContain(
'This is example documentation.'
);
});

it('should generate docs manifest for unattached-mdx entries', async () => {
Expand All @@ -179,18 +192,16 @@ describe('experimental_manifests', () => {
const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult;

expect(result).toHaveProperty('docs');
expect(result.docs).toEqual({
v: 0,
docs: {
'standalone--docs': {
id: 'standalone--docs',
name: 'docs',
path: './Standalone.mdx',
title: 'Standalone',
content: '# Standalone\n\nThis is standalone documentation.',
},
},
expect(result.docs?.docs['standalone--docs']).toMatchObject({
id: 'standalone--docs',
name: 'docs',
path: './Standalone.mdx',
title: 'Standalone',
summary: 'A standalone documentation page',
});
expect(result.docs?.docs['standalone--docs'].content).toContain(
'This is standalone documentation.'
);
});

it('should handle both attached and unattached docs entries separately', async () => {
Expand Down Expand Up @@ -237,20 +248,22 @@ describe('experimental_manifests', () => {
expect(result).toHaveProperty('docs');
expect(result.docs?.docs).toHaveProperty('standalone--docs');
expect(Object.keys(result.docs?.docs ?? {})).toHaveLength(1);
expect(result.docs?.docs['standalone--docs'].content).toBe(
'# Standalone\n\nThis is standalone documentation.'
expect(result.docs?.docs['standalone--docs'].content).toContain(
'This is standalone documentation.'
);
expect(result.docs?.docs['standalone--docs'].summary).toBe('A standalone documentation page');

// Attached docs should be in the component manifest
expect(result.components?.components.example.docs).toEqual({
'example--docs': {
id: 'example--docs',
name: 'docs',
path: './Example.mdx',
title: 'Example',
content: '# Example\n\nThis is example documentation.',
},
expect(result.components?.components.example.docs?.['example--docs']).toMatchObject({
id: 'example--docs',
name: 'docs',
path: './Example.mdx',
title: 'Example',
summary: 'An example documentation page',
});
expect(result.components?.components.example.docs?.['example--docs'].content).toContain(
'This is example documentation.'
);
});

it('should preserve existing manifests and add unattached docs', async () => {
Expand Down Expand Up @@ -347,9 +360,10 @@ describe('experimental_manifests', () => {
expect(Object.keys(result.docs?.docs ?? {})).toHaveLength(2);

// Successful entry
expect(result.docs?.docs['standalone--docs'].content).toBe(
'# Standalone\n\nThis is standalone documentation.'
expect(result.docs?.docs['standalone--docs'].content).toContain(
'This is standalone documentation.'
);
expect(result.docs?.docs['standalone--docs'].summary).toBe('A standalone documentation page');
expect(result.docs?.docs['standalone--docs'].error).toBeUndefined();

// Failed entry
Expand All @@ -359,4 +373,88 @@ describe('experimental_manifests', () => {
message: expect.stringContaining('ENOENT'),
});
});

it('should include summary in unattached docs entries when available', async () => {
const manifestEntries: IndexEntry[] = [
{
id: 'standalone--docs',
name: 'docs',
title: 'Standalone',
type: 'docs',
importPath: './Standalone.mdx',
tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX],
storiesImports: [],
} satisfies DocsIndexEntry,
];

const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult;

expect(result).toHaveProperty('docs');
expect(result.docs?.docs['standalone--docs'].summary).toBe('A standalone documentation page');
});

it('should include summary in attached docs entries when available', async () => {
const existingManifests = {
components: {
v: 0,
components: {
example: {
id: 'example',
path: './Example.stories.tsx',
name: 'Example',
stories: [],
jsDocTags: {},
},
},
},
};
const manifestEntries: IndexEntry[] = [
{
id: 'example--docs',
name: 'docs',
title: 'Example',
type: 'docs',
importPath: './Example.mdx',
tags: [Tag.MANIFEST, Tag.ATTACHED_MDX],
storiesImports: ['./Example.stories.tsx'],
} satisfies DocsIndexEntry,
];

const result = (await manifests(existingManifests, {
manifestEntries,
} as any)) as ManifestResult;

expect(result.components?.components.example.docs?.['example--docs'].summary).toBe(
'An example documentation page'
);
});

it('should not include summary property when analyze returns no summary', async () => {
vol.fromJSON(
{
'./NoSummary.mdx': '# No Summary\n\nThis content has no summary.',
},
'/app'
);

const manifestEntries: IndexEntry[] = [
{
id: 'nosummary--docs',
name: 'docs',
title: 'NoSummary',
type: 'docs',
importPath: './NoSummary.mdx',
tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX],
storiesImports: [],
} satisfies DocsIndexEntry,
];

const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult;

expect(result).toHaveProperty('docs');
expect(result.docs?.docs['nosummary--docs'].content).toBe(
'# No Summary\n\nThis content has no summary.'
);
expect(result.docs?.docs['nosummary--docs'].summary).toBeUndefined();
});
});
12 changes: 11 additions & 1 deletion code/addons/docs/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path';

import { groupBy } from 'storybook/internal/common';
import { Tag } from 'storybook/internal/core-server';
import { Tag, analyzeMdx } from 'storybook/internal/core-server';
import { logger } from 'storybook/internal/node-logger';
import type {
ComponentManifest,
Expand All @@ -20,6 +20,7 @@ export interface DocsManifestEntry {
path: Path;
title: string;
content?: string;
summary?: string;
error?: { name: string; message: string };
}

Expand Down Expand Up @@ -47,12 +48,21 @@ export async function createDocsManifestEntry(entry: DocsIndexEntry): Promise<Do
const absolutePath = path.join(process.cwd(), entry.importPath);
try {
const content = await fs.readFile(absolutePath, 'utf-8');

/*
TODO: This isn't the most performant option, as we're already analyzing the MDX file
during story index generation, and analyzing it requires compiling the file.
We should find a way to only do it once and cache/access the analysis somehow
*/
const { summary } = await analyzeMdx(content);

return {
id: entry.id,
name: entry.name,
path: entry.importPath,
title: entry.title,
content,
...(summary && { summary }),
};
} catch (err) {
return {
Expand Down
2 changes: 1 addition & 1 deletion code/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@
"@react-stately/tabs": "^3.8.5",
"@react-types/shared": "^3.32.0",
"@rolldown/pluginutils": "1.0.0-beta.18",
"@storybook/docs-mdx": "4.0.0-next.1",
"@storybook/docs-mdx": "4.0.0-next.3",
"@tanstack/react-virtual": "^3.3.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^14.0.0",
Expand Down
1 change: 1 addition & 0 deletions code/core/src/core-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { StoryIndexGenerator } from './utils/StoryIndexGenerator';
export { loadStorybook as experimental_loadStorybook } from './load';

export { Tag } from '../shared/constants/tags';
export { analyze as analyzeMdx } from '@storybook/docs-mdx';

export { UniversalStore as experimental_UniversalStore } from '../shared/universal-store';
export { MockUniversalStore as experimental_MockUniversalStore } from '../shared/universal-store/mock';
Expand Down
4 changes: 2 additions & 2 deletions code/core/src/core-server/utils/StoryIndexGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import type {
StorybookConfigRaw,
} from 'storybook/internal/types';

import { analyze } from '@storybook/docs-mdx';

import * as find from 'empathic/find';
import picocolors from 'picocolors';
// eslint-disable-next-line depend/ban-dependencies
Expand Down Expand Up @@ -526,8 +528,6 @@ export class StoryIndexGenerator {
const importPath = slash(normalizedPath);

const content = await readFile(absolutePath, { encoding: 'utf8' });

const { analyze } = await import('@storybook/docs-mdx');
const result = await analyze(content);

// Templates are not indexed
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8070,12 +8070,12 @@ __metadata:
languageName: unknown
linkType: soft

"@storybook/docs-mdx@npm:4.0.0-next.1":
version: 4.0.0-next.1
resolution: "@storybook/docs-mdx@npm:4.0.0-next.1"
"@storybook/docs-mdx@npm:4.0.0-next.3":
version: 4.0.0-next.3
resolution: "@storybook/docs-mdx@npm:4.0.0-next.3"
dependencies:
acorn: "npm:^8.12.1"
checksum: 10c0/8779279014a0a48c00d5884d310b3ca7828a49057c7403371e4eaf0fd053d8c93a412084cbbd6e5ea65e509e27f96752e8de7dadacdfa89198158b8b10deabdc
checksum: 10c0/7d9e689df6a098b0c294f3e835a5a1323460c80b8a33195f811f7de215c6abaf452988851a219ebd59fa472107747b8f54ab2b82d6044c81a76a7e4dc09e506c
languageName: node
linkType: hard

Expand Down Expand Up @@ -28424,7 +28424,7 @@ __metadata:
"@react-stately/tabs": "npm:^3.8.5"
"@react-types/shared": "npm:^3.32.0"
"@rolldown/pluginutils": "npm:1.0.0-beta.18"
"@storybook/docs-mdx": "npm:4.0.0-next.1"
"@storybook/docs-mdx": "npm:4.0.0-next.3"
"@storybook/global": "npm:^5.0.0"
"@storybook/icons": "npm:^2.0.1"
"@tanstack/react-virtual": "npm:^3.3.0"
Expand Down