Skip to content

Commit

Permalink
Fix Storybook source not working with index.json (#625)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshwooding authored Jul 11, 2024
1 parent ea3c272 commit 5e0b006
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 38 deletions.
5 changes: 5 additions & 0 deletions .changeset/eighty-apes-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@jpmorganchase/mosaic-source-storybook': patch
---

Fix storybook source not working with index.json
22 changes: 19 additions & 3 deletions docs/configure/sources/source-storybook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ sidebar:

The Storybook source is used to pull individual stories from Storybook, based on tags.

The Mosaic source will filter your Storybook's stories, based on `kind` or `tags`.
The Mosaic source will filter your Storybook's stories, based on `title` or `tags`.

For each matching story, a page is created in the Mosaic file-system.

Expand All @@ -29,7 +29,7 @@ The Storybook source is an [`HttpSource`](./https-source) and shares the same ba
The `transformResponseToPagesModulePath` prop has a default transformer which creates a page for each matching Story.

The `stories` option is an array of Storybook urls that are used as Sources.
Each story is matched on `kind` using the `filter` Regexp (or by `filterTags`).
Each story is matched on `title` using the `filter` Regexp (or by `filterTags`).

If specified, `additionalTags` and `additionalData` can be added to any matching pages.

Expand Down Expand Up @@ -69,10 +69,26 @@ sources: [
source: 'STORYBOOK'
}
},
filter: /Lab\/Tabs/ // this is a Regexp that matches on Storybook kind
filter: /Lab\/Tabs/ // this is a Regexp that matches on Storybook title
}
]
}
}
]
```

### Example story metadata format

```
{
"type": "story",
"id": "folder--story-1",
"name": "Story 1",
"title": "Folder/Story 1",
"importPath": "./folder/story-1.stories.tsx",
"tags": [
"dev",
"test"
]
}
```
2 changes: 2 additions & 0 deletions packages/source-storybook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"@jpmorganchase/mosaic-source-http": "^0.1.0-beta.80",
"@jpmorganchase/mosaic-schemas": "^0.1.0-beta.80",
"@jpmorganchase/mosaic-types": "^0.1.0-beta.80",
"deepmerge": "^4.2.2",
"lodash-es": "^4.17.21",
"rxjs": "^7.5.5",
"zod": "^3.22.3"
},
Expand Down
39 changes: 27 additions & 12 deletions packages/source-storybook/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,32 @@ const options: StorybookSourceOptions = {
]
};

const createResponse = (index: number) => ({
const createStoriesResponse = (index: number) => ({
v: 3,
stories: {
[`component${index}`]: {
id: `component${index}Id`,
title: `Component ${index} Title`,
name: `Component ${index} Name`,
title: `TestComponent/SomePath/Component-${index}`,
importPath: `./some-path/to/component-${index}`,
tags: ['tag-1', 'tag-2'],
storiesImports: [],
kind: `TestComponent/SomePath/Component-${index}`,
story: `Docs ${index}`
story: `Docs ${index}`,
parameters: {}
}
}
});

const createIndexResponse = (index: number) => ({
v: 5,
entries: {
[`component${index}`]: {
type: `story`,
id: `component${index}Id`,
name: `Component ${index} Name`,
title: `TestComponent/SomePath/Component-${index}`,
importPath: `./some-path/to/component-${index}`,
tags: ['tag-1', 'tag-2']
}
}
});
Expand All @@ -81,32 +96,32 @@ const createExpectedResult = (index: number) => ({
fullPath: `prefixDir/component${index}Id.json`,
tags: [`some-additional-tag-${index}`],
data: {
type: 'story',
id: `component${index}Id`,
contentUrl: `https://storybook.endpoint.com/${index}/iframe.html?id=component${index}Id&viewMode=story&shortcuts=false&singleStory=true`,
description: `some description ${index}`,
kind: `TestComponent/SomePath/Component-${index}`,
title: `TestComponent/SomePath/Component-${index}`,
link: `https://storybook.endpoint.com/${index}?id=component${index}Id`,
name: `Component ${index} Name`,
owner: `some owner ${index}`,
story: `Docs ${index}`
owner: `some owner ${index}`
}
});

const successHandlers = [
rest.get(options.stories[0].storiesUrl, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json(createResponse(1)));
return res(ctx.status(200), ctx.json(createStoriesResponse(1)));
}),
rest.get(options.stories[1].storiesUrl, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json(createResponse(2)));
return res(ctx.status(200), ctx.json(createIndexResponse(2)));
}),
rest.get(options.stories[2].storiesUrl, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json(createResponse(3)));
return res(ctx.status(200), ctx.json(createStoriesResponse(3)));
}),
rest.get(options.stories[3].storiesUrl, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json(createResponse(4)));
return res(ctx.status(200), ctx.json(createIndexResponse(4)));
}),
rest.get(options.stories[4].storiesUrl, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json(createResponse(5)));
return res(ctx.status(200), ctx.json(createStoriesResponse(5)));
})
];

Expand Down
30 changes: 18 additions & 12 deletions packages/source-storybook/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import {
httpSourceCreatorSchema,
createProxyAgent
} from '@jpmorganchase/mosaic-source-http';
import deepmerge from 'deepmerge';

import { StoriesResponseJSON, StorybookPage, StoryConfig } from './types/index.js';
import deepmerge from 'deepmerge';
import { normalizeStorybookJson } from './stories.js';

const baseSchema = httpSourceCreatorSchema.omit({
endpoints: true, // will be generated from the url in the stories object,
Expand Down Expand Up @@ -49,30 +50,35 @@ const transformStorybookPages = (
storyConfig: StoryConfig[]
): StorybookPage[] => {
const { meta = {}, description, filter, filterTags, storyUrlPrefix } = storyConfig[index];
const storyIds = Object.keys(storyJSON.stories);
const normalizedJson = normalizeStorybookJson(storyJSON);
const storyIds = Object.keys(normalizedJson.entries);
return storyIds.reduce<StorybookPage[]>((result, storyId) => {
const story = storyJSON.stories[storyId];
if (filter && !filter.test(story.kind)) {
const story = normalizedJson.entries[storyId];
const { id, type, name, title: storyTitle, tags } = story;
if (filter && !filter.test(storyTitle)) {
return result;
}
if (filterTags && filterTags.some(filterTag => story.tags.indexOf(filterTag) >= 0)) {
if (
filterTags &&
filterTags.some(filterTag => Array.isArray(tags) && tags.indexOf(filterTag) >= 0)
) {
return result;
}
const { id, kind, name, story: storyName } = story;
const title = `${kind} - ${name}`;

const title = `${storyTitle} - ${name}`;
const route = `${prefixDir}/${id}`;
let storyPageMeta: StorybookPage = {
title,
route,
fullPath: `${route}.json`,
data: {
type,
id,
name,
title: storyTitle,
description,
kind,
contentUrl: `${storyUrlPrefix}/iframe.html?id=${id}&viewMode=story&shortcuts=false&singleStory=true`,
link: `${storyUrlPrefix}?id=${id}`,
name,
story: storyName
link: `${storyUrlPrefix}?id=${id}`
}
};
if (meta) {
Expand Down Expand Up @@ -102,7 +108,7 @@ const StorybookSource: Source<StorybookSourceOptions, StorybookPage> = {
console.log(`[Mosaic] Storybook source using ${proxyEndpoint} proxy for ${storiesUrl}`);
agent = createProxyAgent(proxyEndpoint);
}
const url = storiesUrl || `${storyUrlPrefix}/stories.json`;
const url = storiesUrl || `${storyUrlPrefix}/index.json`;
return new Request(url, {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Expand Down
82 changes: 82 additions & 0 deletions packages/source-storybook/src/stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Transformers taken from https://github.com/storybookjs/storybook/blob/34ea7ba592ff06f6fbd31e3af95aa2efdfab2397/code/core/src/manager-api/lib/stories.ts#L52
import { countBy } from 'lodash-es';
import type {
StoriesResponseJSON,
StoryIndexV2,
StoryIndexV3,
API_PreparedStoryIndex,
IndexEntry
} from './types/index.js';

export const transformStoryIndexV2toV3 = (index: StoryIndexV2): StoryIndexV3 => ({
v: 3,
stories: Object.values(index.stories).reduce((acc, entry) => {
acc[entry.id] = {
...entry,
title: entry.kind,
name: entry.name || entry.story,
importPath: entry.parameters.fileName || ''
};

return acc;
}, {} as StoryIndexV3['stories'])
});

export const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStoryIndex => {
const countByTitle = countBy(Object.values(index.stories), 'title');
return {
v: 4,
entries: Object.values(index.stories).reduce((acc, entry: any) => {
let type: IndexEntry['type'] = 'story';
if (
entry.parameters?.docsOnly ||
(entry.name === 'Page' && countByTitle[entry.title] === 1)
) {
type = 'docs';
}
acc[entry.id] = {
type,
...(type === 'docs' && { tags: ['stories-mdx'], storiesImports: [] }),
...entry
};

// @ts-expect-error (we're removing something that should not be there)
delete acc[entry.id].story;
// @ts-expect-error (we're removing something that should not be there)
delete acc[entry.id].kind;

return acc;
}, {} as API_PreparedStoryIndex['entries'])
};
};

/**
* Storybook 8.0 and below did not automatically tag stories with 'dev'.
* Therefore Storybook 8.1 and above would not show composed 8.0 stories by default.
* This function adds the 'dev' tag to all stories in the index to workaround this issue.
*/
export const transformStoryIndexV4toV5 = (
index: API_PreparedStoryIndex
): API_PreparedStoryIndex => ({
v: 5,
entries: Object.values(index.entries).reduce((acc, entry) => {
acc[entry.id] = {
...entry,
tags: entry.tags ? ['dev', 'test', ...entry.tags] : ['dev']
};

return acc;
}, {} as API_PreparedStoryIndex['entries'])
});

export const normalizeStorybookJson = (input: StoriesResponseJSON): API_PreparedStoryIndex => {
if (!input.v) {
return {} as API_PreparedStoryIndex;
}

let index = input;
index = index.v === 2 ? transformStoryIndexV2toV3(index as never) : index;
index = index.v === 3 ? transformStoryIndexV3toV4(index as never) : index;
index = index.v === 4 ? transformStoryIndexV4toV5(index as never) : index;
return index as API_PreparedStoryIndex;
};
57 changes: 46 additions & 11 deletions packages/source-storybook/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Page } from '@jpmorganchase/mosaic-types';

/** Storybook Source config - added to mosaic.config.mjs **/
/* Storybook Source config - added to mosaic.config.mjs */
export type StoryConfig = {
storiesUrl?: string;
storyUrlPrefix: string;
Expand All @@ -10,28 +10,63 @@ export type StoryConfig = {
meta?: Partial<StorybookPage>;
};

/** Storybook API response */
export type StoryResponseJSON = {
/* Storybook types inlined and simplified from @storybook/core/types */
interface Parameters {
[name: string]: any;
}

interface BaseIndexEntry {
id: string;
kind: string;
name: string;
story: string;
title: string;
tags: string[];
tags?: string[];
importPath: string;
}
type StoryIndexEntry = BaseIndexEntry & {
type: 'story';
};
export type StoriesResponseJSON = {
stories: Record<string, StoryResponseJSON>;
type DocsIndexEntry = BaseIndexEntry & {
storiesImports: string[];
type: 'docs';
};
export type IndexEntry = StoryIndexEntry | DocsIndexEntry;

// eslint-disable-next-line @typescript-eslint/naming-convention
export interface API_PreparedStoryIndex {
v: number;
entries: Record<string, IndexEntry>;
}

interface V3CompatIndexEntry extends Omit<StoryIndexEntry, 'type' | 'tags'> {
kind: string;
story: string;
parameters: Parameters;
}
export interface StoryIndexV2 {
v: number;
stories: Record<
string,
Omit<V3CompatIndexEntry, 'title' | 'name' | 'importPath'> & {
name?: string;
}
>;
}
export interface StoryIndexV3 {
v: number;
stories: Record<string, V3CompatIndexEntry>;
}

export type StoriesResponseJSON = StoryIndexV2 | StoryIndexV3 | API_PreparedStoryIndex;

/** Storybook page data */
export type StorybookPageData = {
type: 'story' | 'docs';
id: string;
contentUrl: string;
description: string;
contentUrl: string;
link: string;
kind: string;
title: string;
name: string;
story: string;
};

/** Page created by the Source for each Storybook story */
Expand Down

0 comments on commit 5e0b006

Please sign in to comment.