From d9e46dd1f3534e95b1a989f37d6ee960ef312ece Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 22 Jul 2022 11:08:04 +1000 Subject: [PATCH 1/4] Support DocsPage in v6 store --- code/lib/api/src/lib/stories.ts | 44 ++++++++--- code/lib/api/src/modules/refs.ts | 6 +- code/lib/api/src/modules/stories.ts | 6 +- code/lib/api/src/tests/stories.test.ts | 73 +++++++++++++++++++ .../src/preview/iframe-webpack.config.ts | 4 +- code/lib/client-api/src/StoryStoreFacade.ts | 54 +++++++++++--- .../lib/core-client/src/preview/start.test.ts | 3 + 7 files changed, 162 insertions(+), 28 deletions(-) diff --git a/code/lib/api/src/lib/stories.ts b/code/lib/api/src/lib/stories.ts index 08404b3af2c3..b8f2e63925f0 100644 --- a/code/lib/api/src/lib/stories.ts +++ b/code/lib/api/src/lib/stories.ts @@ -5,7 +5,7 @@ import { dedent } from 'ts-dedent'; import mapValues from 'lodash/mapValues'; import countBy from 'lodash/countBy'; import global from 'global'; -import type { +import { StoryId, ComponentTitle, StoryKind, @@ -13,8 +13,11 @@ import type { Args, ArgTypes, Parameters, + toId, + ComponentId, + sanitize, } from '@storybook/csf'; -import { sanitize } from '@storybook/csf'; +import type { DocsOptions } from '@storybook/core-common'; import { combineParameters } from '../index'; import merge from './merge'; @@ -145,6 +148,7 @@ export interface SetStoriesStory { id: StoryId; name: string; refId?: string; + componentId?: ComponentId; kind: StoryKind; parameters: { fileName: string; @@ -268,16 +272,21 @@ export interface PreparedStoryIndex { export const transformSetStoriesStoryDataToStoriesHash = ( data: SetStoriesStoryData, - { provider, docsMode }: { provider: Provider; docsMode: boolean } + { provider, docsOptions }: { provider: Provider; docsOptions: DocsOptions } ) => - transformStoryIndexToStoriesHash(transformSetStoriesStoryDataToPreparedStoryIndex(data), { - provider, - docsMode, - }); + transformStoryIndexToStoriesHash( + transformSetStoriesStoryDataToPreparedStoryIndex(data, { docsOptions }), + { + provider, + docsOptions, + } + ); const transformSetStoriesStoryDataToPreparedStoryIndex = ( - stories: SetStoriesStoryData + stories: SetStoriesStoryData, + { docsOptions }: { docsOptions: DocsOptions } ): PreparedStoryIndex => { + const seenTitles = new Set(); const entries: PreparedStoryIndex['entries'] = Object.entries(stories).reduce( (acc, [id, story]) => { if (!story) return acc; @@ -296,6 +305,19 @@ const transformSetStoriesStoryDataToPreparedStoryIndex = ( ...base, }; } else { + if (!seenTitles.has(base.title) && docsOptions.docsPage) { + const name = docsOptions.defaultName; + const docsId = toId(story.componentId || base.title, name); + seenTitles.add(base.title); + acc[docsId] = { + type: 'docs', + storiesImports: [], + ...base, + id: docsId, + name, + }; + } + const { argTypes, args, initialArgs } = story; acc[id] = { type: 'story', @@ -340,10 +362,10 @@ export const transformStoryIndexToStoriesHash = ( index: PreparedStoryIndex, { provider, - docsMode, + docsOptions, }: { provider: Provider; - docsMode: boolean; + docsOptions: DocsOptions; } ): StoriesHash => { if (!index.v) throw new Error('Composition: Missing stories.json version'); @@ -364,7 +386,7 @@ export const transformStoryIndexToStoriesHash = ( } const storiesHashOutOfOrder = Object.values(entryValues).reduce((acc, item) => { - if (docsMode && item.type !== 'docs') return acc; + if (docsOptions.docsMode && item.type !== 'docs') return acc; // First, split the title into a set of names, separated by '/' and trimmed. const { title } = item; diff --git a/code/lib/api/src/modules/refs.ts b/code/lib/api/src/modules/refs.ts index 1a0d0ff316d9..98c1e5fce683 100644 --- a/code/lib/api/src/modules/refs.ts +++ b/code/lib/api/src/modules/refs.ts @@ -133,7 +133,7 @@ const map = ( }; export const init: ModuleFn = ( - { store, provider, singleStory, docsOptions: { docsMode } = {} }, + { store, provider, singleStory, docsOptions = {} }, { runCheck = true } = {} ) => { const api: SubAPI = { @@ -249,10 +249,10 @@ export const init: ModuleFn = ( if (setStoriesData) { storiesHash = transformSetStoriesStoryDataToStoriesHash( map(setStoriesData, ref, { storyMapper }), - { provider, docsMode } + { provider, docsOptions } ); } else if (storyIndex) { - storiesHash = transformStoryIndexToStoriesHash(storyIndex, { provider, docsMode }); + storiesHash = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions }); } if (storiesHash) storiesHash = addRefIds(storiesHash, ref); diff --git a/code/lib/api/src/modules/stories.ts b/code/lib/api/src/modules/stories.ts index e280a22fd67f..6a9f9f1813db 100644 --- a/code/lib/api/src/modules/stories.ts +++ b/code/lib/api/src/modules/stories.ts @@ -122,7 +122,7 @@ export const init: ModuleFn = ({ provider, storyId: initialStoryId, viewMode: initialViewMode, - docsOptions: { docsMode } = {}, + docsOptions = {}, }) => { const api: SubAPI = { storyId: toId, @@ -211,7 +211,7 @@ export const init: ModuleFn = ({ // Now create storiesHash by reordering the above by group const hash = transformSetStoriesStoryDataToStoriesHash(input, { provider, - docsMode, + docsOptions, }); await store.setState({ @@ -359,7 +359,7 @@ export const init: ModuleFn = ({ setStoryList: async (storyIndex: StoryIndex) => { const hash = transformStoryIndexToStoriesHash(storyIndex, { provider, - docsMode, + docsOptions, }); await store.setState({ diff --git a/code/lib/api/src/tests/stories.test.ts b/code/lib/api/src/tests/stories.test.ts index 9005c86d2e63..cffa81cbaac8 100644 --- a/code/lib/api/src/tests/stories.test.ts +++ b/code/lib/api/src/tests/stories.test.ts @@ -430,6 +430,79 @@ describe('stories API', () => { children: ['b--1'], }); }); + + it('adds docs entries when docsPage is enabled', () => { + const navigate = jest.fn(); + const store = createMockStore({}); + + const { + api: { setStories }, + } = initStories({ + store, + navigate, + provider, + docsOptions: { docsPage: true, defaultName: 'Docs' }, + } as any as any); + + provider.getConfig.mockReturnValue({ sidebar: { showRoots: false } }); + setStories(setStoriesData); + + const { storiesHash: storedStoriesHash } = store.getState(); + + // We need exact key ordering, even if in theory JS doesn't guarantee it + expect(Object.keys(storedStoriesHash)).toEqual([ + 'a', + 'a--docs', + 'a--1', + 'a--2', + 'b', + 'b-c', + 'b-c--docs', + 'b-c--1', + 'b-d', + 'b-d--docs', + 'b-d--1', + 'b-d--2', + 'b-e', + 'b-e--docs', + 'custom-id--1', + ]); + expect(storedStoriesHash['a--docs']).toMatchObject({ + type: 'docs', + id: 'a--docs', + parent: 'a', + title: 'a', + name: 'Docs', + storiesImports: [], + }); + + expect(storedStoriesHash['b-c--docs']).toMatchObject({ + type: 'docs', + id: 'b-c--docs', + parent: 'b-c', + title: 'b/c', + name: 'Docs', + storiesImports: [], + }); + + expect(storedStoriesHash['b-d--docs']).toMatchObject({ + type: 'docs', + id: 'b-d--docs', + parent: 'b-d', + title: 'b/d', + name: 'Docs', + storiesImports: [], + }); + + expect(storedStoriesHash['b-e--docs']).toMatchObject({ + type: 'docs', + id: 'b-e--docs', + parent: 'b-e', + title: 'b/e', + name: 'Docs', + storiesImports: [], + }); + }); }); // Can't currently run these tests as cannot set this on the events diff --git a/code/lib/builder-webpack5/src/preview/iframe-webpack.config.ts b/code/lib/builder-webpack5/src/preview/iframe-webpack.config.ts index 479da574dcd6..19ccb2759216 100644 --- a/code/lib/builder-webpack5/src/preview/iframe-webpack.config.ts +++ b/code/lib/builder-webpack5/src/preview/iframe-webpack.config.ts @@ -9,7 +9,7 @@ import TerserWebpackPlugin from 'terser-webpack-plugin'; import VirtualModulePlugin from 'webpack-virtual-modules'; import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; -import type { Options, CoreConfig } from '@storybook/core-common'; +import type { Options, CoreConfig, DocsOptions } from '@storybook/core-common'; import { stringifyProcessEnvs, handlebars, @@ -87,6 +87,7 @@ export default async ( const coreOptions = await presets.apply('core'); const builderOptions: BuilderOptions = typeof coreOptions.builder === 'string' ? {} : coreOptions.builder?.options || {}; + const docsOptions = await presets.apply('docs'); const configs = [ ...(await presets.apply('config', [], options)), @@ -214,6 +215,7 @@ export default async ( ...specifier, importPathMatcher: specifier.importPathMatcher.source, })), + DOCS_OPTIONS: docsOptions, SERVER_CHANNEL_URL: serverChannelUrl, }, headHtmlSnippet, diff --git a/code/lib/client-api/src/StoryStoreFacade.ts b/code/lib/client-api/src/StoryStoreFacade.ts index 0499f1520b4f..dd7c3f35c077 100644 --- a/code/lib/client-api/src/StoryStoreFacade.ts +++ b/code/lib/client-api/src/StoryStoreFacade.ts @@ -2,7 +2,13 @@ import global from 'global'; import { dedent } from 'ts-dedent'; import { SynchronousPromise } from 'synchronous-promise'; -import { toId, isExportStory, storyNameFromExport } from '@storybook/csf'; +import { + toId, + isExportStory, + storyNameFromExport, + ComponentTitle, + ComponentId, +} from '@storybook/csf'; import type { StoryId, AnyFramework, Parameters, StoryFn } from '@storybook/csf'; import { StoryStore, userOrAutoTitle, sortStoriesV6 } from '@storybook/store'; import type { @@ -16,6 +22,7 @@ import type { } from '@storybook/store'; import { logger } from '@storybook/client-logger'; import deprecate from 'util-deprecate'; +import type { DocsOptions } from '@storybook/core-common'; export interface GetStorybookStory { name: string; @@ -34,7 +41,7 @@ const docs2Warning = deprecate(() => {}, export class StoryStoreFacade { projectAnnotations: NormalizedProjectAnnotations; - entries: StoryIndex['entries']; + entries: Record; csfExports: Record; @@ -71,19 +78,27 @@ export class StoryStoreFacade { const storyEntries = Object.entries(this.entries); // Add the kind parameters and global parameters to each entry const sortableV6: [StoryId, Story, Parameters, Parameters][] = storyEntries.map( - ([storyId, { importPath }]) => { + ([storyId, { type, importPath, ...entry }]) => { const exports = this.csfExports[importPath]; const csfFile = store.processCSFFileWithCache( exports, importPath, exports.default.title ); - return [ - storyId, - store.storyFromCSFFile({ storyId, csfFile }), - csfFile.meta.parameters, - this.projectAnnotations.parameters, - ]; + + let storyLike: Story; + if (type === 'story') { + storyLike = store.storyFromCSFFile({ storyId, csfFile }); + } else { + storyLike = { + ...entry, + story: entry.name, + kind: entry.title, + componentId: toId(entry.componentId || entry.title), + parameters: { fileName: importPath }, + } as any; + } + return [storyId, storyLike, csfFile.meta.parameters, this.projectAnnotations.parameters]; } ); @@ -188,6 +203,8 @@ export class StoryStoreFacade { }); } + const docsOptions = global.DOCS_OPTIONS as DocsOptions; + const seenTitles = new Set(); Object.entries(sortedExports) .filter(([key]) => isExportStory(key, defaultExport)) .forEach(([key, storyExport]: [string, any]) => { @@ -199,12 +216,29 @@ export class StoryStoreFacade { storyExport.story?.name || exportName; + if (!seenTitles.has(title) && docsOptions.docsPage) { + const name = docsOptions.defaultName; + const docsId = toId(componentId || title, name); + seenTitles.add(title); + this.entries[docsId] = { + type: 'docs', + standalone: false, + id: docsId, + title, + name, + importPath: fileName, + storiesImports: [], + componentId, + }; + } + this.entries[id] = { + type: 'story', id, name, title, importPath: fileName, - type: 'story', + componentId, }; }); } diff --git a/code/lib/core-client/src/preview/start.test.ts b/code/lib/core-client/src/preview/start.test.ts index 07e33406c06f..9811d4df182a 100644 --- a/code/lib/core-client/src/preview/start.test.ts +++ b/code/lib/core-client/src/preview/start.test.ts @@ -31,6 +31,9 @@ jest.mock('global', () => ({ FEATURES: { breakingChangesV7: true, }, + DOCS_OPTIONS: { + docsPage: false, + }, })); jest.mock('@storybook/channel-postmessage', () => ({ createChannel: () => mockChannel })); From f381f9c46b33f3c0f4ad559728fb3997bc3e927e Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 25 Jul 2022 14:48:44 +1000 Subject: [PATCH 2/4] Split types + code --- code/lib/api/src/lib/stories.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/lib/api/src/lib/stories.ts b/code/lib/api/src/lib/stories.ts index b8f2e63925f0..9be760fe3d8f 100644 --- a/code/lib/api/src/lib/stories.ts +++ b/code/lib/api/src/lib/stories.ts @@ -5,7 +5,8 @@ import { dedent } from 'ts-dedent'; import mapValues from 'lodash/mapValues'; import countBy from 'lodash/countBy'; import global from 'global'; -import { +import { toId, sanitize } from '@storybook/csf'; +import type { StoryId, ComponentTitle, StoryKind, @@ -13,9 +14,7 @@ import { Args, ArgTypes, Parameters, - toId, ComponentId, - sanitize, } from '@storybook/csf'; import type { DocsOptions } from '@storybook/core-common'; From b4bb90933cae005f8141ce94e852cb4a631574a5 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 25 Jul 2022 15:02:19 +1000 Subject: [PATCH 3/4] Fix storyshots tests --- code/lib/client-api/src/StoryStoreFacade.ts | 2 +- code/lib/core-client/src/preview/start.test.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/code/lib/client-api/src/StoryStoreFacade.ts b/code/lib/client-api/src/StoryStoreFacade.ts index dd7c3f35c077..dc037df6c481 100644 --- a/code/lib/client-api/src/StoryStoreFacade.ts +++ b/code/lib/client-api/src/StoryStoreFacade.ts @@ -203,7 +203,7 @@ export class StoryStoreFacade { }); } - const docsOptions = global.DOCS_OPTIONS as DocsOptions; + const docsOptions = (global.DOCS_OPTIONS || {}) as DocsOptions; const seenTitles = new Set(); Object.entries(sortedExports) .filter(([key]) => isExportStory(key, defaultExport)) diff --git a/code/lib/core-client/src/preview/start.test.ts b/code/lib/core-client/src/preview/start.test.ts index 9811d4df182a..07e33406c06f 100644 --- a/code/lib/core-client/src/preview/start.test.ts +++ b/code/lib/core-client/src/preview/start.test.ts @@ -31,9 +31,6 @@ jest.mock('global', () => ({ FEATURES: { breakingChangesV7: true, }, - DOCS_OPTIONS: { - docsPage: false, - }, })); jest.mock('@storybook/channel-postmessage', () => ({ createChannel: () => mockChannel })); From 23a2c7c6a75ec83b33e129e98a7acc714a02a927 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 26 Jul 2022 15:45:55 +1000 Subject: [PATCH 4/4] Heading to a storybook might render in `#docs-root` now --- code/cypress/helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/cypress/helper.ts b/code/cypress/helper.ts index 088c5fdfdf34..2622eb0eb1f0 100644 --- a/code/cypress/helper.ts +++ b/code/cypress/helper.ts @@ -20,7 +20,7 @@ export const visit = (route = '') => { expect(element).not.null; if (element !== null) { - expect(element.querySelector('#root > *')).not.null; + expect(element.querySelector('#root > *, #docs-root > *')).not.null; } }); });