From feca848f1f2425897510f23bb773c45a84467d69 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 2 May 2022 15:33:03 +1000 Subject: [PATCH 01/16] WIP - got docs entry loading working --- addons/docs/src/blocks/DocsContext.ts | 4 +- addons/docs/src/blocks/Meta.tsx | 3 + examples/react-ts/src/docs2/MetaOf.docs.mdx | 1 + lib/preview-web/src/DocsRender.ts | 67 ++++++++++---- lib/preview-web/src/PreviewWeb.tsx | 96 ++++++++++++--------- lib/preview-web/src/StoryRender.ts | 4 + lib/preview-web/src/renderDocs.tsx | 41 +++++++-- lib/store/src/StoryIndexStore.ts | 14 ++- lib/store/src/StoryStore.ts | 19 ++++ lib/store/src/types.ts | 1 + 10 files changed, 184 insertions(+), 66 deletions(-) diff --git a/addons/docs/src/blocks/DocsContext.ts b/addons/docs/src/blocks/DocsContext.ts index c902d8fb9b2f..86d483eabaae 100644 --- a/addons/docs/src/blocks/DocsContext.ts +++ b/addons/docs/src/blocks/DocsContext.ts @@ -13,10 +13,10 @@ export type { DocsContextProps }; // This was specifically a problem with the Vite builder. /* eslint-disable no-underscore-dangle */ if (globalWindow && globalWindow.__DOCS_CONTEXT__ === undefined) { - globalWindow.__DOCS_CONTEXT__ = createContext({}); + globalWindow.__DOCS_CONTEXT__ = createContext(null); globalWindow.__DOCS_CONTEXT__.displayName = 'DocsContext'; } export const DocsContext: Context> = globalWindow ? globalWindow.__DOCS_CONTEXT__ - : createContext({}); + : createContext(null); diff --git a/addons/docs/src/blocks/Meta.tsx b/addons/docs/src/blocks/Meta.tsx index 87c375d0796f..ad6c0d73ca0a 100644 --- a/addons/docs/src/blocks/Meta.tsx +++ b/addons/docs/src/blocks/Meta.tsx @@ -16,6 +16,9 @@ function getFirstStoryId(docsContext: DocsContextProps): string { function renderAnchor() { const context = useContext(DocsContext); + if (!context) { + return null; + } const anchorId = getFirstStoryId(context) || context.id; return ; diff --git a/examples/react-ts/src/docs2/MetaOf.docs.mdx b/examples/react-ts/src/docs2/MetaOf.docs.mdx index b5451cafaea1..bce8c8ca29c6 100644 --- a/examples/react-ts/src/docs2/MetaOf.docs.mdx +++ b/examples/react-ts/src/docs2/MetaOf.docs.mdx @@ -1,3 +1,4 @@ +import { Meta } from '@storybook/addon-docs'; import meta from '../button.stories'; diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts index d386611584e6..ffa8ce35d4e8 100644 --- a/lib/preview-web/src/DocsRender.ts +++ b/lib/preview-web/src/DocsRender.ts @@ -1,35 +1,59 @@ import global from 'global'; import { AnyFramework, StoryId, ViewMode, StoryContextForLoaders } from '@storybook/csf'; -import { Story, StoryStore, CSFFile } from '@storybook/store'; +import { Story, StoryStore, CSFFile, ModuleExports, IndexEntry } from '@storybook/store'; import { Channel } from '@storybook/addons'; import { DOCS_RENDERED } from '@storybook/core-events'; -import { Render, StoryRender } from './StoryRender'; +import { Render, RenderType } from './StoryRender'; import type { DocsContextProps } from './types'; export class DocsRender implements Render { + public type: RenderType = 'docs'; + + public id: StoryId; + + private legacy: boolean; + + public story?: Story; + + public exports?: ModuleExports; + + private preparing = false; + private canvasElement?: HTMLElement; private context?: DocsContextProps; public disableKeyListeners = false; - static fromStoryRender(storyRender: StoryRender) { - const { channel, store, id, story } = storyRender; - return new DocsRender(channel, store, id, story); - } - - // eslint-disable-next-line no-useless-constructor constructor( private channel: Channel, private store: StoryStore, - public id: StoryId, - public story: Story - ) {} + public entry: IndexEntry + ) { + this.id = entry.id; + this.legacy = entry.type === 'story' || entry.legacy; + } + + // The two story "renders" are equal and have both loaded the same story + isEqual(other?: Render) { + return other && this.id === other.id && this.legacy + ? this.story && this.story === other.story + : other.type === 'docs' && this.entry === (other as DocsRender).entry; + } + + async prepare() { + this.preparing = true; + if (this.legacy) { + this.story = await this.store.loadStory({ storyId: this.id }); + } else { + this.exports = await this.store.loadDocsFileById(this.id); + } + this.preparing = false; + } - // DocsRender doesn't prepare, it is created *from* a prepared StoryRender isPreparing() { - return false; + return this.preparing; } async renderToElement( @@ -38,7 +62,7 @@ export class DocsRender implements Render = await this.store.loadCSFFileByStoryId(this.id); this.context = { @@ -63,13 +87,20 @@ export class DocsRender implements Render - this.channel.emit(DOCS_RENDERED, this.id) - ); + + if (this.legacy) { + renderer.renderLegacyDocs(this.story, this.context, this.canvasElement, () => + this.channel.emit(DOCS_RENDERED, this.id) + ); + } else { + renderer.renderDocs(this.exports, this.context, this.canvasElement, () => + this.channel.emit(DOCS_RENDERED, this.id) + ); + } } async rerender() { diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx index 417730034bd3..0aea42fb8902 100644 --- a/lib/preview-web/src/PreviewWeb.tsx +++ b/lib/preview-web/src/PreviewWeb.tsx @@ -39,7 +39,7 @@ export class PreviewWeb extends Preview; + currentRender: StoryRender | DocsRender; constructor() { super(); @@ -227,12 +227,17 @@ export class PreviewWeb extends Preview extends Preview( - this.channel, - this.storyStore, - (...args) => { - // At the start of renderToDOM we make the story visible (see note in WebView) - this.view.showStoryDuringRender(); - return this.renderToDOM(...args); - }, - this.mainStoryCallbacks(storyId), - storyId, - 'story' - ); + let render; + if (viewMode === 'story') { + render = new StoryRender( + this.channel, + this.storyStore, + (...args) => { + // At the start of renderToDOM we make the story visible (see note in WebView) + this.view.showStoryDuringRender(); + return this.renderToDOM(...args); + }, + this.mainStoryCallbacks(storyId), + storyId, + 'story' + ); + } else { + render = new DocsRender(this.channel, this.storyStore, entry); + } + console.log(render); + // We need to store this right away, so if the story changes during // the async `.prepare()` below, we can (potentially) cancel it this.currentSelection = selection; - // Note this may be replaced by a docsRender after preparing - this.currentRender = storyRender; + this.currentRender = render; try { - await storyRender.prepare(); + await render.prepare(); } catch (err) { if (err !== PREPARE_ABORTED) { // We are about to render an error so make sure the previous story is @@ -281,11 +292,10 @@ export class PreviewWeb extends Preview extends Preview + ).context(); + if (global.FEATURES?.storyStoreV7) { + this.channel.emit(Events.STORY_PREPARED, { + id: storyId, + parameters, + initialArgs, + argTypes, + args, + }); + } - // For v6 mode / compatibility - // If the implementation changed, or args were persisted, the args may have changed, - // and the STORY_PREPARED event above may not be respected. - if (implementationChanged || persistedArgs) { - this.channel.emit(Events.STORY_ARGS_UPDATED, { storyId, args }); + // For v6 mode / compatibility + // If the implementation changed, or args were persisted, the args may have changed, + // and the STORY_PREPARED event above may not be respected. + if (implementationChanged || persistedArgs) { + this.channel.emit(Events.STORY_ARGS_UPDATED, { storyId, args }); + } } - if (selection.viewMode === 'docs' || parameters.docsOnly) { - this.currentRender = DocsRender.fromStoryRender(storyRender); + if (viewMode === 'docs') { this.currentRender.renderToElement( this.view.prepareForDocs(), this.renderStoryToElement.bind(this) ); } else { - this.storyRenders.push(storyRender); - this.currentRender.renderToElement(this.view.prepareForStory(storyRender.story)); + this.storyRenders.push(render as StoryRender); + (this.currentRender as StoryRender).renderToElement( + this.view.prepareForStory(render.story) + ); } } diff --git a/lib/preview-web/src/StoryRender.ts b/lib/preview-web/src/StoryRender.ts index d0ede7cc1223..bbcafc70a592 100644 --- a/lib/preview-web/src/StoryRender.ts +++ b/lib/preview-web/src/StoryRender.ts @@ -40,7 +40,9 @@ export type RenderContextCallbacks = Pick< export const PREPARE_ABORTED = new Error('prepareAborted'); +export type RenderType = 'story' | 'docs'; export interface Render { + type: RenderType; id: StoryId; story?: Story; isPreparing: () => boolean; @@ -50,6 +52,8 @@ export interface Render { } export class StoryRender implements Render { + public type: RenderType = 'story'; + public story?: Story; public phase?: RenderPhase; diff --git a/lib/preview-web/src/renderDocs.tsx b/lib/preview-web/src/renderDocs.tsx index e75204ac032c..e3839df47164 100644 --- a/lib/preview-web/src/renderDocs.tsx +++ b/lib/preview-web/src/renderDocs.tsx @@ -1,21 +1,30 @@ -import React, { ComponentType } from 'react'; +import React, { ComponentType, ReactElement } from 'react'; import ReactDOM from 'react-dom'; import { AnyFramework } from '@storybook/csf'; -import { Story } from '@storybook/store'; +import { ModuleExports, Story } from '@storybook/store'; import { DocsContextProps } from './types'; import { NoDocs } from './NoDocs'; -export function renderDocs( +export function renderLegacyDocs( story: Story, docsContext: DocsContextProps, element: HTMLElement, callback: () => void ) { - return renderDocsAsync(story, docsContext, element).then(callback); + return renderLegacyDocsAsync(story, docsContext, element).then(callback); } -async function renderDocsAsync( +export function renderDocs( + exports: ModuleExports, + docsContext: DocsContextProps, + element: HTMLElement, + callback: () => void +) { + return renderDocsAsync(exports, docsContext, element).then(callback); +} + +async function renderLegacyDocsAsync( story: Story, docsContext: DocsContextProps, element: HTMLElement @@ -45,6 +54,28 @@ async function renderDocsAsync( }); } +async function renderDocsAsync( + exports: ModuleExports, + docsContext: DocsContextProps, + element: HTMLElement +) { + // FIXME -- is this at all correct? + const DocsContainer = ({ children }: { children: ReactElement }) => <>{children}; + + const Page = exports.default; + + // FIXME -- do we need to set a key as above? + const docsElement = ( + + + + ); + + await new Promise((resolve) => { + ReactDOM.render(docsElement, element, resolve); + }); +} + export function unmountDocs(element: HTMLElement) { ReactDOM.unmountComponentAtNode(element); } diff --git a/lib/store/src/StoryIndexStore.ts b/lib/store/src/StoryIndexStore.ts index 0ee9b338f460..98292c2eca2b 100644 --- a/lib/store/src/StoryIndexStore.ts +++ b/lib/store/src/StoryIndexStore.ts @@ -1,8 +1,16 @@ import dedent from 'ts-dedent'; import { Channel } from '@storybook/addons'; import type { StoryId } from '@storybook/csf'; +import memoize from 'memoizerific'; -import type { StorySpecifier, StoryIndex, IndexEntry } from './types'; +import type { StorySpecifier, StoryIndex, IndexEntry, Path } from './types'; + +const getImportPathMap = memoize(1)((entries: StoryIndex['entries']) => + Object.values(entries).reduce((acc, entry) => { + acc[entry.importPath] = acc[entry.importPath] || entry; + return acc; + }, {} as Record) +); export class StoryIndexStore { channel: Channel; @@ -50,4 +58,8 @@ export class StoryIndexStore { return storyEntry; } + + importPathToEntry(importPath: Path): IndexEntry { + return getImportPathMap(this.entries)[importPath]; + } } diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts index 0f307a46bff6..384e4fe5a10f 100644 --- a/lib/store/src/StoryStore.ts +++ b/lib/store/src/StoryStore.ts @@ -31,6 +31,7 @@ import type { StoryIndexEntry, V2CompatIndexEntry, StoryIndexV3, + ModuleExports, } from './types'; import { HooksContext } from './hooks'; @@ -120,6 +121,24 @@ export class StoryStore { if (this.cachedCSFFiles) await this.cacheAllCSFFiles(); } + // FIXME: does this need to be load + async loadDocsFileById(docsId: StoryId): Promise { + const entry = this.storyIndex.storyIdToEntry(docsId); + if (entry.type !== 'docs') throw new Error(`Cannot load docs file for id ${docsId}`); + + const { importPath, storiesImports } = entry; + + const [docsImport] = await Promise.all([ + this.importFn(importPath), + ...storiesImports.map((storyImportPath) => { + const firstStoryEntry = this.storyIndex.importPathToEntry(storyImportPath); + return this.loadCSFFileByStoryId(firstStoryEntry.id); + }), + ]); + + return docsImport; + } + // To load a single CSF file to service a story we need to look up the importPath in the index loadCSFFileByStoryId(storyId: StoryId): PromiseLike> { const { importPath, title } = this.storyIndex.storyIdToEntry(storyId); diff --git a/lib/store/src/types.ts b/lib/store/src/types.ts index f11a587c2216..a39ac1e28cc5 100644 --- a/lib/store/src/types.ts +++ b/lib/store/src/types.ts @@ -101,6 +101,7 @@ export type StoryIndexEntry = BaseIndexEntry & { export type DocsIndexEntry = BaseIndexEntry & { storiesImports: Path[]; type: 'docs'; + legacy?: boolean; }; export type IndexEntry = StoryIndexEntry | DocsIndexEntry; From f7690afb173f596eb9907f99465fc590d9293361 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Tue, 3 May 2022 21:37:17 +1000 Subject: [PATCH 02/16] WIP Got Story Rendering working --- addons/docs/src/blocks/DocsRenderer.tsx | 56 +++++++++++ addons/docs/src/blocks/Meta.tsx | 2 +- .../src => addons/docs/src/blocks}/NoDocs.tsx | 0 addons/docs/src/blocks/Story.tsx | 10 +- addons/docs/src/blocks/useStory.ts | 2 +- addons/docs/src/preview.ts | 7 +- examples/react-ts/src/docs2/MetaOf.docs.mdx | 6 +- lib/preview-web/package.json | 4 - lib/preview-web/src/DocsRender.ts | 94 +++++++++++++------ lib/preview-web/src/PreviewWeb.tsx | 1 - lib/preview-web/src/renderDocs.tsx | 81 ---------------- lib/preview-web/src/types.ts | 31 +++--- lib/store/src/StoryStore.ts | 13 ++- lib/store/src/csf/processCSFFile.ts | 15 ++- yarn.lock | 12 +-- 15 files changed, 185 insertions(+), 149 deletions(-) create mode 100644 addons/docs/src/blocks/DocsRenderer.tsx rename {lib/preview-web/src => addons/docs/src/blocks}/NoDocs.tsx (100%) delete mode 100644 lib/preview-web/src/renderDocs.tsx diff --git a/addons/docs/src/blocks/DocsRenderer.tsx b/addons/docs/src/blocks/DocsRenderer.tsx new file mode 100644 index 000000000000..c38a1c8d0e81 --- /dev/null +++ b/addons/docs/src/blocks/DocsRenderer.tsx @@ -0,0 +1,56 @@ +import React, { ComponentType, ReactElement } from 'react'; +import ReactDOM from 'react-dom'; +import { AnyFramework, Parameters } from '@storybook/csf'; +import { ModuleExports, Story } from '@storybook/store'; +import type { DocsRenderFunction } from '@storybook/preview-web'; +import { DocsContainer } from './DocsContainer'; +import { DocsPage } from './DocsPage'; + +import { DocsContext, DocsContextProps } from './DocsContext'; +import { NoDocs } from './NoDocs'; + +// FIXME -- make this: DocsRenderFunction +export function renderDocs( + docsContext: DocsContextProps, + docsParameters: Parameters, + element: HTMLElement, + callback: () => void +): void { + renderDocsAsync(docsContext, docsParameters, element).then(callback); +} + +async function renderDocsAsync( + docsContext: DocsContextProps, + docsParameters: Parameters, + element: HTMLElement +) { + console.log(docsParameters); + + // FIXME -- use DocsContainer, make it work for modern + const SimpleContainer = ({ children }: any) => ( + {children} + ); + + const Container: ComponentType<{ context: DocsContextProps }> = + docsParameters.container || + (await docsParameters.getContainer?.()) || + (docsContext.legacy ? DocsContainer : SimpleContainer); + + const Page: ComponentType = docsParameters.page || (await docsParameters.getPage?.()) || DocsPage; + console.log(docsParameters.page, Page); + + // Use `title` as a key so that we force a re-render every time we switch components + const docsElement = ( + + + + ); + + await new Promise((resolve) => { + ReactDOM.render(docsElement, element, resolve); + }); +} + +export function unmountDocs(element: HTMLElement) { + ReactDOM.unmountComponentAtNode(element); +} diff --git a/addons/docs/src/blocks/Meta.tsx b/addons/docs/src/blocks/Meta.tsx index ad6c0d73ca0a..ed906936f791 100644 --- a/addons/docs/src/blocks/Meta.tsx +++ b/addons/docs/src/blocks/Meta.tsx @@ -16,7 +16,7 @@ function getFirstStoryId(docsContext: DocsContextProps): string { function renderAnchor() { const context = useContext(DocsContext); - if (!context) { + if (!context.legacy) { return null; } const anchorId = getFirstStoryId(context) || context.id; diff --git a/lib/preview-web/src/NoDocs.tsx b/addons/docs/src/blocks/NoDocs.tsx similarity index 100% rename from lib/preview-web/src/NoDocs.tsx rename to addons/docs/src/blocks/NoDocs.tsx diff --git a/addons/docs/src/blocks/Story.tsx b/addons/docs/src/blocks/Story.tsx index e61fcb321512..e9b5ba79ef3e 100644 --- a/addons/docs/src/blocks/Story.tsx +++ b/addons/docs/src/blocks/Story.tsx @@ -36,6 +36,7 @@ type StoryDefProps = { type StoryRefProps = { id?: string; + of?: any; }; type StoryImportProps = { @@ -55,7 +56,12 @@ export const lookupStoryId = ( ); export const getStoryId = (props: StoryProps, context: DocsContextProps): StoryId => { - const { id } = props as StoryRefProps; + const { id, of } = props as StoryRefProps; + + if (of) { + return context.storyIdByModuleExport(of); + } + const { name } = props as StoryDefProps; const inputId = id === CURRENT_SELECTION ? context.id : id; return inputId || lookupStoryId(name, context); @@ -127,6 +133,8 @@ const Story: FunctionComponent = (props) => { const story = useStory(storyId, context); const [showLoader, setShowLoader] = useState(true); + console.log(storyId, story); + useEffect(() => { let cleanup: () => void; if (story && storyRef.current) { diff --git a/addons/docs/src/blocks/useStory.ts b/addons/docs/src/blocks/useStory.ts index 6a58d0472262..2e73f7d63c2e 100644 --- a/addons/docs/src/blocks/useStory.ts +++ b/addons/docs/src/blocks/useStory.ts @@ -16,7 +16,7 @@ export function useStories( storyIds: StoryId[], context: DocsContextProps ): (Story | void)[] { - const initialStoriesById = context.componentStories().reduce((acc, story) => { + const initialStoriesById = context.preloadedStories().reduce((acc, story) => { acc[story.id] = story; return acc; }, {} as Record>); diff --git a/addons/docs/src/preview.ts b/addons/docs/src/preview.ts index a6269505f285..00ed5271d263 100644 --- a/addons/docs/src/preview.ts +++ b/addons/docs/src/preview.ts @@ -1,6 +1,9 @@ export const parameters = { docs: { - getContainer: async () => (await import('./blocks')).DocsContainer, - getPage: async () => (await import('./blocks')).DocsPage, + renderer: async () => { + const x = await import('./blocks/DocsRenderer'); + console.log(x); + return x; + }, }, }; diff --git a/examples/react-ts/src/docs2/MetaOf.docs.mdx b/examples/react-ts/src/docs2/MetaOf.docs.mdx index bce8c8ca29c6..2f5a1c9d4277 100644 --- a/examples/react-ts/src/docs2/MetaOf.docs.mdx +++ b/examples/react-ts/src/docs2/MetaOf.docs.mdx @@ -1,8 +1,10 @@ -import { Meta } from '@storybook/addon-docs'; -import meta from '../button.stories'; +import { Meta, Story } from '@storybook/addon-docs'; +import meta, { Basic } from '../button.stories'; # Docs with of hello docs + + diff --git a/lib/preview-web/package.json b/lib/preview-web/package.json index 3c4e94b6c406..f294182e88db 100644 --- a/lib/preview-web/package.json +++ b/lib/preview-web/package.json @@ -57,10 +57,6 @@ "unfetch": "^4.2.0", "util-deprecate": "^1.0.2" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, "publishConfig": { "access": "public" }, diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts index ffa8ce35d4e8..1f1e0f56a144 100644 --- a/lib/preview-web/src/DocsRender.ts +++ b/lib/preview-web/src/DocsRender.ts @@ -5,7 +5,7 @@ import { Channel } from '@storybook/addons'; import { DOCS_RENDERED } from '@storybook/core-events'; import { Render, RenderType } from './StoryRender'; -import type { DocsContextProps } from './types'; +import type { DocsContextProps, DocsRenderFunction } from './types'; export class DocsRender implements Render { public type: RenderType = 'docs'; @@ -22,10 +22,12 @@ export class DocsRender implements Render; public disableKeyListeners = false; + public teardown: (options: { viewModeChanged?: boolean }) => Promise; + constructor( private channel: Channel, private store: StoryStore, @@ -56,22 +58,17 @@ export class DocsRender implements Render['renderStoryToElement'] + ): Promise> { const { id, title, name } = this.entry; const csfFile: CSFFile = await this.store.loadCSFFileByStoryId(this.id); - this.context = { + const base = { + legacy: this.legacy, id, title, name, - // NOTE: these two functions are *sync* so cannot access stories from other CSF files - storyById: (storyId: StoryId) => this.store.storyFromCSFFile({ storyId, csfFile }), - componentStories: () => this.store.componentStoriesFromCSFFile({ csfFile }), loadStory: (storyId: StoryId) => this.store.loadStory({ storyId }), renderStoryToElement, getStoryContext: (renderedStory: Story) => @@ -79,10 +76,45 @@ export class DocsRender implements Render), - // Put all the storyContext fields onto the docs context for back-compat - ...(!global.FEATURES?.breakingChangesV7 && this.store.getStoryContext(this.story)), }; + if (this.legacy) { + const componentStories = () => this.store.componentStoriesFromCSFFile({ csfFile }); + return { + ...base, + + // NOTE: these two functions are *sync* so cannot access stories from other CSF files + storyIdByModuleExport: () => { + throw new Error('`storyIdByModuleExport` not available for legacy docs files.'); + }, + storyById: (storyId: StoryId) => this.store.storyFromCSFFile({ storyId, csfFile }), + + componentStories, + preloadedStories: componentStories, + }; + } + + return { + ...base, + storyIdByModuleExport: (moduleExport) => this.store.storyIdByModuleExport({ moduleExport }), + storyById: () => { + throw new Error('`storyById` not available for modern docs files.'); + }, + + componentStories: () => { + throw new Error('You cannot render all the stories for a component in a docs.mdx file'); + }, + preloadedStories: () => [], // FIXME + }; + } + + async renderToElement( + canvasElement: HTMLElement, + renderStoryToElement: DocsContextProps['renderStoryToElement'] + ) { + this.canvasElement = canvasElement; + this.context = await this.docsContext(renderStoryToElement); + return this.render(); } @@ -90,17 +122,29 @@ export class DocsRender implements Render - this.channel.emit(DOCS_RENDERED, this.id) - ); - } else { - renderer.renderDocs(this.exports, this.context, this.canvasElement, () => - this.channel.emit(DOCS_RENDERED, this.id) + if (!docs) { + throw new Error( + `Cannot render a story in viewMode=docs if \`@storybook/addon-docs\` is not installed` ); } + + const renderer = await docs.renderer(); + (renderer.renderDocs as DocsRenderFunction)( + this.context, + { + ...docs, + ...(!this.legacy && { page: this.exports.default }), + }, + this.canvasElement, + () => this.channel.emit(DOCS_RENDERED, this.id) + ); + this.teardown = async ({ viewModeChanged }: { viewModeChanged?: boolean } = {}) => { + if (!viewModeChanged || !this.canvasElement) return; + // TODO type + renderer.unmountDocs(this.canvasElement); + }; } async rerender() { @@ -110,10 +154,4 @@ export class DocsRender implements Render extends Preview(this.channel, this.storyStore, entry); } - console.log(render); // We need to store this right away, so if the story changes during // the async `.prepare()` below, we can (potentially) cancel it diff --git a/lib/preview-web/src/renderDocs.tsx b/lib/preview-web/src/renderDocs.tsx deleted file mode 100644 index e3839df47164..000000000000 --- a/lib/preview-web/src/renderDocs.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import React, { ComponentType, ReactElement } from 'react'; -import ReactDOM from 'react-dom'; -import { AnyFramework } from '@storybook/csf'; -import { ModuleExports, Story } from '@storybook/store'; - -import { DocsContextProps } from './types'; -import { NoDocs } from './NoDocs'; - -export function renderLegacyDocs( - story: Story, - docsContext: DocsContextProps, - element: HTMLElement, - callback: () => void -) { - return renderLegacyDocsAsync(story, docsContext, element).then(callback); -} - -export function renderDocs( - exports: ModuleExports, - docsContext: DocsContextProps, - element: HTMLElement, - callback: () => void -) { - return renderDocsAsync(exports, docsContext, element).then(callback); -} - -async function renderLegacyDocsAsync( - story: Story, - docsContext: DocsContextProps, - element: HTMLElement -) { - const { docs } = story.parameters; - if ((docs?.getPage || docs?.page) && !(docs?.getContainer || docs?.container)) { - throw new Error('No `docs.container` set, did you run `addon-docs/preset`?'); - } - - const DocsContainer: ComponentType<{ context: DocsContextProps }> = - docs.container || - (await docs.getContainer?.()) || - (({ children }: { children: Element }) => <>{children}); - - const Page: ComponentType = docs.page || (await docs.getPage?.()) || NoDocs; - - // Use `componentId` as a key so that we force a re-render every time - // we switch components - const docsElement = ( - - - - ); - - await new Promise((resolve) => { - ReactDOM.render(docsElement, element, resolve); - }); -} - -async function renderDocsAsync( - exports: ModuleExports, - docsContext: DocsContextProps, - element: HTMLElement -) { - // FIXME -- is this at all correct? - const DocsContainer = ({ children }: { children: ReactElement }) => <>{children}; - - const Page = exports.default; - - // FIXME -- do we need to set a key as above? - const docsElement = ( - - - - ); - - await new Promise((resolve) => { - ReactDOM.render(docsElement, element, resolve); - }); -} - -export function unmountDocs(element: HTMLElement) { - ReactDOM.unmountComponentAtNode(element); -} diff --git a/lib/preview-web/src/types.ts b/lib/preview-web/src/types.ts index c068a57a8150..ffa80c91fa73 100644 --- a/lib/preview-web/src/types.ts +++ b/lib/preview-web/src/types.ts @@ -4,21 +4,27 @@ import type { AnyFramework, StoryContextForLoaders, ComponentTitle, - Args, - Globals, + Parameters, } from '@storybook/csf'; import type { Story } from '@storybook/store'; import { PreviewWeb } from './PreviewWeb'; export interface DocsContextProps { + legacy: boolean; + id: StoryId; title: ComponentTitle; name: StoryName; + + storyIdByModuleExport: (moduleExport: any) => StoryId; storyById: (id: StoryId) => Story; + getStoryContext: (story: Story) => StoryContextForLoaders; + componentStories: () => Story[]; + preloadedStories: () => Story[]; + loadStory: (id: StoryId) => Promise>; renderStoryToElement: PreviewWeb['renderStoryToElement']; - getStoryContext: (story: Story) => StoryContextForLoaders; /** * mdxStoryNameToKey is an MDX-compiler-generated mapping of an MDX story's @@ -27,16 +33,11 @@ export interface DocsContextProps; mdxComponentAnnotations?: any; - - // These keys are deprecated and will be removed in v7 - /** @deprecated */ - kind?: ComponentTitle; - /** @deprecated */ - story?: StoryName; - /** @deprecated */ - args?: Args; - /** @deprecated */ - globals?: Globals; - /** @deprecated */ - parameters?: Globals; } + +export type DocsRenderFunction = ( + docsContext: DocsContextProps, + docsParameters: Parameters, + element: HTMLElement, + callback: () => void +) => void; diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts index 384e4fe5a10f..37a00642c72d 100644 --- a/lib/store/src/StoryStore.ts +++ b/lib/store/src/StoryStore.ts @@ -62,6 +62,11 @@ export class StoryStore { resolveInitializationPromise: () => void; + /** + * A map of module export to story id, for later consumption + */ + moduleExportMap: Map = new Map(); + constructor() { this.globals = new GlobalsStore(); this.args = new ArgsStore(); @@ -144,7 +149,7 @@ export class StoryStore { const { importPath, title } = this.storyIndex.storyIdToEntry(storyId); return this.importFn(importPath).then((moduleExports) => // We pass the title in here as it may have been generated by autoTitle on the server. - this.processCSFFileWithCache(moduleExports, importPath, title) + this.processCSFFileWithCache(moduleExports, importPath, title, this.moduleExportMap) ); } @@ -184,6 +189,12 @@ export class StoryStore { return this.storyFromCSFFile({ storyId, csfFile }); } + storyIdByModuleExport({ moduleExport }: { moduleExport: any }) { + if (this.moduleExportMap.has(moduleExport)) return this.moduleExportMap.get(moduleExport); + + throw new Error(`Couldn't find story for that export: ${moduleExport}.`); + } + // This function is synchronous for convenience -- often times if you have a CSF file already // it is easier not to have to await `loadStory`. storyFromCSFFile({ diff --git a/lib/store/src/csf/processCSFFile.ts b/lib/store/src/csf/processCSFFile.ts index 02bf81c918cd..6102d27e99b5 100644 --- a/lib/store/src/csf/processCSFFile.ts +++ b/lib/store/src/csf/processCSFFile.ts @@ -1,10 +1,16 @@ -import type { Parameters, AnyFramework, ComponentTitle } from '@storybook/csf'; +import type { Parameters, AnyFramework, ComponentTitle, StoryId } from '@storybook/csf'; import { isExportStory } from '@storybook/csf'; import { logger } from '@storybook/client-logger'; import { normalizeStory } from './normalizeStory'; import { normalizeComponentAnnotations } from './normalizeComponentAnnotations'; -import type { ModuleExports, CSFFile, NormalizedComponentAnnotations, Path } from '../types'; +import type { + ModuleExports, + CSFFile, + NormalizedComponentAnnotations, + Path, + NormalizedStoryAnnotations, +} from '../types'; const checkGlobals = (parameters: Parameters) => { const { globals, globalTypes } = parameters; @@ -36,7 +42,8 @@ const checkDisallowedParameters = (parameters: Parameters) => { export function processCSFFile( moduleExports: ModuleExports, importPath: Path, - title: ComponentTitle + title: ComponentTitle, + moduleExportMap?: Map ): CSFFile { const { default: defaultExport, __namedExportsOrder, ...namedExports } = moduleExports; @@ -51,6 +58,8 @@ export function processCSFFile( const storyMeta = normalizeStory(key, namedExports[key], meta); checkDisallowedParameters(storyMeta.parameters); + // Track which exports get turned into which ids + if (moduleExportMap) moduleExportMap.set(namedExports[key], storyMeta.id); csfFile.stories[storyMeta.id] = storyMeta; } }); diff --git a/yarn.lock b/yarn.lock index 8c1cadb92efe..96fed7eac1f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8631,14 +8631,11 @@ __metadata: ts-dedent: ^2.0.0 unfetch: ^4.2.0 util-deprecate: ^1.0.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 languageName: unknown linkType: soft "@storybook/preview-web@npm:6.5.0-beta.1": - version: 6.5.0-beta.1 + version: 0.0.0-use.local resolution: "@storybook/preview-web@npm:6.5.0-beta.1" dependencies: "@storybook/addons": 6.5.0-beta.1 @@ -8657,12 +8654,9 @@ __metadata: ts-dedent: ^2.0.0 unfetch: ^4.2.0 util-deprecate: ^1.0.2 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 checksum: 0ea1e2d1b295b1f07c462b497dc878ac4f61c5eb810f16a0b6e923869f7b4e591af783df7edacd555178f1b4a41eebe789c3e259973299a81f0dda699c6e3581 - languageName: node - linkType: hard + languageName: unknown + linkType: soft "@storybook/react-docgen-typescript-plugin@npm:1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0": version: 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 From 82926f558a536dd14ebd67d95ddc959b56555600 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Wed, 4 May 2022 16:25:49 +1000 Subject: [PATCH 03/16] Refactor so that we store `moduleExports` on story --- addons/docs/src/blocks/Story.tsx | 2 -- addons/docs/src/blocks/useStory.ts | 20 +++++++++++++------- examples/nextra-demo | 1 + lib/preview-web/src/DocsRender.ts | 27 ++++++++++++++++++++------- lib/preview-web/src/types.ts | 1 - lib/store/src/StoryStore.ts | 25 ++++++++----------------- lib/store/src/csf/normalizeStory.ts | 1 + lib/store/src/csf/prepareStory.ts | 3 ++- lib/store/src/csf/processCSFFile.ts | 13 ++----------- lib/store/src/types.ts | 5 ++++- 10 files changed, 51 insertions(+), 47 deletions(-) create mode 160000 examples/nextra-demo diff --git a/addons/docs/src/blocks/Story.tsx b/addons/docs/src/blocks/Story.tsx index e9b5ba79ef3e..370d465d51a6 100644 --- a/addons/docs/src/blocks/Story.tsx +++ b/addons/docs/src/blocks/Story.tsx @@ -133,8 +133,6 @@ const Story: FunctionComponent = (props) => { const story = useStory(storyId, context); const [showLoader, setShowLoader] = useState(true); - console.log(storyId, story); - useEffect(() => { let cleanup: () => void; if (story && storyRef.current) { diff --git a/addons/docs/src/blocks/useStory.ts b/addons/docs/src/blocks/useStory.ts index 2e73f7d63c2e..a9caa893001d 100644 --- a/addons/docs/src/blocks/useStory.ts +++ b/addons/docs/src/blocks/useStory.ts @@ -16,12 +16,9 @@ export function useStories( storyIds: StoryId[], context: DocsContextProps ): (Story | void)[] { - const initialStoriesById = context.preloadedStories().reduce((acc, story) => { - acc[story.id] = story; - return acc; - }, {} as Record>); - - const [storiesById, setStories] = useState(initialStoriesById as typeof initialStoriesById); + // Legacy docs pages can reference any story by id. Those stories will need to be + // asyncronously loaded; we use the state for this + const [storiesById, setStories] = useState>>({}); useEffect(() => { Promise.all( @@ -40,5 +37,14 @@ export function useStories( ); }); - return storyIds.map((storyId) => storiesById[storyId]); + return storyIds.map((storyId) => { + if (storiesById[storyId]) return storiesById[storyId]; + + try { + // If we are allowed to load this story id synchonously, this will work + return context.storyById(storyId); + } catch (err) { + return null; + } + }); } diff --git a/examples/nextra-demo b/examples/nextra-demo new file mode 160000 index 000000000000..8d6b70bfdaf2 --- /dev/null +++ b/examples/nextra-demo @@ -0,0 +1 @@ +Subproject commit 8d6b70bfdaf25998a9b1d1fd4d2498948fd88a0e diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts index 1f1e0f56a144..b73e4411888e 100644 --- a/lib/preview-web/src/DocsRender.ts +++ b/lib/preview-web/src/DocsRender.ts @@ -18,6 +18,8 @@ export class DocsRender implements Render[]; + private preparing = false; private canvasElement?: HTMLElement; @@ -49,7 +51,9 @@ export class DocsRender implements Render implements Render['renderStoryToElement'] ): Promise> { const { id, title, name } = this.entry; - const csfFile: CSFFile = await this.store.loadCSFFileByStoryId(this.id); const base = { legacy: this.legacy, @@ -79,32 +82,42 @@ export class DocsRender implements Render = await this.store.loadCSFFileByStoryId(this.id); const componentStories = () => this.store.componentStoriesFromCSFFile({ csfFile }); return { ...base, - // NOTE: these two functions are *sync* so cannot access stories from other CSF files storyIdByModuleExport: () => { + // NOTE: we could implement this easily enough by checking all the component stories throw new Error('`storyIdByModuleExport` not available for legacy docs files.'); }, storyById: (storyId: StoryId) => this.store.storyFromCSFFile({ storyId, csfFile }), componentStories, - preloadedStories: componentStories, }; } return { ...base, - storyIdByModuleExport: (moduleExport) => this.store.storyIdByModuleExport({ moduleExport }), + storyIdByModuleExport: (moduleExport) => { + // eslint-disable-next-line no-restricted-syntax + for (const csfFile of this.csfFiles) { + // eslint-disable-next-line no-restricted-syntax + for (const annotation of Object.values(csfFile.stories)) { + if (annotation.moduleExport === moduleExport) { + return annotation.id; + } + } + } + + throw new Error(`No story found with that export: ${moduleExport}`); + }, storyById: () => { throw new Error('`storyById` not available for modern docs files.'); }, - componentStories: () => { throw new Error('You cannot render all the stories for a component in a docs.mdx file'); }, - preloadedStories: () => [], // FIXME }; } diff --git a/lib/preview-web/src/types.ts b/lib/preview-web/src/types.ts index ffa80c91fa73..4fd7e83eac81 100644 --- a/lib/preview-web/src/types.ts +++ b/lib/preview-web/src/types.ts @@ -21,7 +21,6 @@ export interface DocsContextProps) => StoryContextForLoaders; componentStories: () => Story[]; - preloadedStories: () => Story[]; loadStory: (id: StoryId) => Promise>; renderStoryToElement: PreviewWeb['renderStoryToElement']; diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts index 37a00642c72d..c1a0912469cf 100644 --- a/lib/store/src/StoryStore.ts +++ b/lib/store/src/StoryStore.ts @@ -62,11 +62,6 @@ export class StoryStore { resolveInitializationPromise: () => void; - /** - * A map of module export to story id, for later consumption - */ - moduleExportMap: Map = new Map(); - constructor() { this.globals = new GlobalsStore(); this.args = new ArgsStore(); @@ -126,22 +121,24 @@ export class StoryStore { if (this.cachedCSFFiles) await this.cacheAllCSFFiles(); } - // FIXME: does this need to be load - async loadDocsFileById(docsId: StoryId): Promise { + // FIXME: does this need to be loaded by extract etc? + async loadDocsFileById( + docsId: StoryId + ): Promise<{ docsExports: ModuleExports; csfFiles: CSFFile[] }> { const entry = this.storyIndex.storyIdToEntry(docsId); if (entry.type !== 'docs') throw new Error(`Cannot load docs file for id ${docsId}`); const { importPath, storiesImports } = entry; - const [docsImport] = await Promise.all([ + const [docsExports, ...csfFiles] = (await Promise.all([ this.importFn(importPath), ...storiesImports.map((storyImportPath) => { const firstStoryEntry = this.storyIndex.importPathToEntry(storyImportPath); return this.loadCSFFileByStoryId(firstStoryEntry.id); }), - ]); + ])) as [ModuleExports, ...CSFFile[]]; - return docsImport; + return { docsExports, csfFiles }; } // To load a single CSF file to service a story we need to look up the importPath in the index @@ -149,7 +146,7 @@ export class StoryStore { const { importPath, title } = this.storyIndex.storyIdToEntry(storyId); return this.importFn(importPath).then((moduleExports) => // We pass the title in here as it may have been generated by autoTitle on the server. - this.processCSFFileWithCache(moduleExports, importPath, title, this.moduleExportMap) + this.processCSFFileWithCache(moduleExports, importPath, title) ); } @@ -189,12 +186,6 @@ export class StoryStore { return this.storyFromCSFFile({ storyId, csfFile }); } - storyIdByModuleExport({ moduleExport }: { moduleExport: any }) { - if (this.moduleExportMap.has(moduleExport)) return this.moduleExportMap.get(moduleExport); - - throw new Error(`Couldn't find story for that export: ${moduleExport}.`); - } - // This function is synchronous for convenience -- often times if you have a CSF file already // it is easier not to have to await `loadStory`. storyFromCSFFile({ diff --git a/lib/store/src/csf/normalizeStory.ts b/lib/store/src/csf/normalizeStory.ts index 37773d9094f9..a1635ab67daf 100644 --- a/lib/store/src/csf/normalizeStory.ts +++ b/lib/store/src/csf/normalizeStory.ts @@ -58,6 +58,7 @@ export function normalizeStory( // eslint-disable-next-line no-underscore-dangle const id = parameters.__id || toId(meta.id || meta.title, exportName); return { + moduleExport: storyAnnotations, id, name, decorators, diff --git a/lib/store/src/csf/prepareStory.ts b/lib/store/src/csf/prepareStory.ts index b797320add0c..6e82d761441a 100644 --- a/lib/store/src/csf/prepareStory.ts +++ b/lib/store/src/csf/prepareStory.ts @@ -48,7 +48,7 @@ export function prepareStory( // anything at render time. The assumption is that as we don't load all the stories at once, this // will have a limited cost. If this proves misguided, we can refactor it. - const { id, name } = storyAnnotations; + const { moduleExport, id, name } = storyAnnotations; const { title } = componentAnnotations; const parameters: Parameters = combineParameters( @@ -198,6 +198,7 @@ export function prepareStory( return Object.freeze({ ...contextForEnhancers, + moduleExport, originalStoryFn: render, undecoratedStoryFn, unboundStoryFn, diff --git a/lib/store/src/csf/processCSFFile.ts b/lib/store/src/csf/processCSFFile.ts index 6102d27e99b5..fd0bad3afacc 100644 --- a/lib/store/src/csf/processCSFFile.ts +++ b/lib/store/src/csf/processCSFFile.ts @@ -4,13 +4,7 @@ import { logger } from '@storybook/client-logger'; import { normalizeStory } from './normalizeStory'; import { normalizeComponentAnnotations } from './normalizeComponentAnnotations'; -import type { - ModuleExports, - CSFFile, - NormalizedComponentAnnotations, - Path, - NormalizedStoryAnnotations, -} from '../types'; +import type { ModuleExports, CSFFile, NormalizedComponentAnnotations, Path } from '../types'; const checkGlobals = (parameters: Parameters) => { const { globals, globalTypes } = parameters; @@ -42,8 +36,7 @@ const checkDisallowedParameters = (parameters: Parameters) => { export function processCSFFile( moduleExports: ModuleExports, importPath: Path, - title: ComponentTitle, - moduleExportMap?: Map + title: ComponentTitle ): CSFFile { const { default: defaultExport, __namedExportsOrder, ...namedExports } = moduleExports; @@ -58,8 +51,6 @@ export function processCSFFile( const storyMeta = normalizeStory(key, namedExports[key], meta); checkDisallowedParameters(storyMeta.parameters); - // Track which exports get turned into which ids - if (moduleExportMap) moduleExportMap.set(namedExports[key], storyMeta.id); csfFile.stories[storyMeta.id] = storyMeta; } }); diff --git a/lib/store/src/types.ts b/lib/store/src/types.ts index a39ac1e28cc5..e8afb7db1757 100644 --- a/lib/store/src/types.ts +++ b/lib/store/src/types.ts @@ -25,7 +25,8 @@ import type { export type { StoryId, Parameters }; export type Path = string; -export type ModuleExports = Record; +export type ModuleExport = any; +export type ModuleExports = Record; export type PromiseLike = Promise | SynchronousPromise; export type ModuleImportFn = (path: Path) => PromiseLike; @@ -51,6 +52,7 @@ export type NormalizedStoryAnnotations, 'storyName' | 'story' > & { + moduleExport: ModuleExport; // You cannot actually set id on story annotations, but we normalize it to be there for convience id: StoryId; argTypes?: StrictArgTypes; @@ -64,6 +66,7 @@ export type CSFFile = { export type Story = StoryContextForEnhancers & { + moduleExport: ModuleExport; originalStoryFn: StoryFn; undecoratedStoryFn: LegacyStoryFn; unboundStoryFn: LegacyStoryFn; From 6eb0a67d3dc47322e256f3df1cce86033445a531 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 5 May 2022 15:11:10 +1000 Subject: [PATCH 04/16] Got store code tested --- lib/store/src/StoryIndexStore.test.ts | 49 +++++++------ lib/store/src/StoryStore.test.ts | 91 +++++++++++++++++++++++- lib/store/src/StoryStore.ts | 48 +++++++------ lib/store/src/csf/normalizeStory.test.ts | 9 ++- lib/store/src/csf/prepareStory.test.ts | 68 +++++++++++++----- lib/store/src/types.ts | 2 +- 6 files changed, 202 insertions(+), 65 deletions(-) diff --git a/lib/store/src/StoryIndexStore.test.ts b/lib/store/src/StoryIndexStore.test.ts index 601e08dde6fb..6aaa1b0e218a 100644 --- a/lib/store/src/StoryIndexStore.test.ts +++ b/lib/store/src/StoryIndexStore.test.ts @@ -7,16 +7,19 @@ const storyIndex: StoryIndex = { v: 4, entries: { 'component-one--a': { + id: 'component-one--a', title: 'Component One', name: 'A', importPath: './src/ComponentOne.stories.js', }, 'component-one--b': { + id: 'component-one--b', title: 'Component One', name: 'B', importPath: './src/ComponentOne.stories.js', }, 'component-two--c': { + id: 'component-one--c', title: 'Component Two', name: 'C', importPath: './src/ComponentTwo.stories.js', @@ -118,35 +121,41 @@ describe('StoryIndexStore', () => { expect(store.storyIdFromSpecifier('a--3')).toEqual('a--3'); }); }); - }); - describe('storyIdToEntry', () => { - it('works when the story exists', async () => { - const store = new StoryIndexStore(storyIndex); + describe('storyIdToEntry', () => { + it('works when the story exists', async () => { + const store = new StoryIndexStore(storyIndex); - expect(store.storyIdToEntry('component-one--a')).toEqual({ - name: 'A', - title: 'Component One', - importPath: './src/ComponentOne.stories.js', + expect(store.storyIdToEntry('component-one--a')).toEqual( + storyIndex.entries['component-one--a'] + ); + expect(store.storyIdToEntry('component-one--b')).toEqual( + storyIndex.entries['component-one--b'] + ); + expect(store.storyIdToEntry('component-two--c')).toEqual( + storyIndex.entries['component-two--c'] + ); }); - expect(store.storyIdToEntry('component-one--b')).toEqual({ - name: 'B', - title: 'Component One', - importPath: './src/ComponentOne.stories.js', - }); + it('throws when the story does not', async () => { + const store = new StoryIndexStore(storyIndex); - expect(store.storyIdToEntry('component-two--c')).toEqual({ - name: 'C', - title: 'Component Two', - importPath: './src/ComponentTwo.stories.js', + expect(() => store.storyIdToEntry('random')).toThrow( + /Couldn't find story matching 'random'/ + ); }); }); + }); - it('throws when the story does not', async () => { + describe('importPathToEntry', () => { + it('works', () => { const store = new StoryIndexStore(storyIndex); - - expect(() => store.storyIdToEntry('random')).toThrow(/Couldn't find story matching 'random'/); + expect(store.importPathToEntry('./src/ComponentOne.stories.js')).toEqual( + storyIndex.entries['component-one--a'] + ); + expect(store.importPathToEntry('./src/ComponentTwo.stories.js')).toEqual( + storyIndex.entries['component-two--c'] + ); }); }); }); diff --git a/lib/store/src/StoryStore.test.ts b/lib/store/src/StoryStore.test.ts index 2f066e976379..b8e73e0e5e1a 100644 --- a/lib/store/src/StoryStore.test.ts +++ b/lib/store/src/StoryStore.test.ts @@ -156,6 +156,8 @@ describe('StoryStore', () => { }); }); + describe('loadDocsFileById', () => {}); + describe('setProjectAnnotations', () => { it('busts the loadStory cache', async () => { const store = new StoryStore(); @@ -330,6 +332,11 @@ describe('StoryStore', () => { "foo": "a", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "a", + }, + }, "name": "A", "parameters": Object { "__isArgsStory": false, @@ -497,6 +504,11 @@ describe('StoryStore', () => { "foo": "a", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "a", + }, + }, "name": "A", "parameters": Object { "__isArgsStory": false, @@ -532,6 +544,11 @@ describe('StoryStore', () => { "foo": "b", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "b", + }, + }, "name": "B", "parameters": Object { "__isArgsStory": false, @@ -567,6 +584,11 @@ describe('StoryStore', () => { "foo": "c", }, "kind": "Component Two", + "moduleExport": Object { + "args": Object { + "foo": "c", + }, + }, "name": "C", "parameters": Object { "__isArgsStory": false, @@ -581,7 +603,7 @@ describe('StoryStore', () => { `); }); - it('does not include docs only stories by default', async () => { + it('does not include (legacy) docs only stories by default', async () => { const docsOnlyImportFn = jest.fn(async (path) => { return path === './src/ComponentOne.stories.js' ? { @@ -607,6 +629,43 @@ describe('StoryStore', () => { 'component-two--c', ]); }); + + it('does not include (modern) docs entries ever', async () => { + const docsOnlyStoryIndex: StoryIndex = { + v: 4, + entries: { + ...storyIndex.entries, + 'introduction--docs': { + type: 'docs', + id: 'introduction--docs', + title: 'Introduction', + name: 'Docs', + importPath: './introduction.docs.mdx', + storiesImports: [], + }, + }, + }; + const store = new StoryStore(); + store.setProjectAnnotations(projectAnnotations); + store.initialize({ + storyIndex: docsOnlyStoryIndex, + importFn, + cache: false, + }); + await store.cacheAllCSFFiles(); + + expect(Object.keys(store.extract())).toEqual([ + 'component-one--a', + 'component-one--b', + 'component-two--c', + ]); + + expect(Object.keys(store.extract({ includeDocsOnly: true }))).toEqual([ + 'component-one--a', + 'component-one--b', + 'component-two--c', + ]); + }); }); describe('raw', () => { @@ -641,6 +700,11 @@ describe('StoryStore', () => { "foo": "a", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "a", + }, + }, "name": "A", "originalStoryFn": [MockFunction], "parameters": Object { @@ -678,6 +742,11 @@ describe('StoryStore', () => { "foo": "b", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "b", + }, + }, "name": "B", "originalStoryFn": [MockFunction], "parameters": Object { @@ -715,6 +784,11 @@ describe('StoryStore', () => { "foo": "c", }, "kind": "Component Two", + "moduleExport": Object { + "args": Object { + "foo": "c", + }, + }, "name": "C", "originalStoryFn": [MockFunction], "parameters": Object { @@ -777,6 +851,11 @@ describe('StoryStore', () => { "foo": "a", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "a", + }, + }, "name": "A", "parameters": Object { "__isArgsStory": false, @@ -812,6 +891,11 @@ describe('StoryStore', () => { "foo": "b", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "b", + }, + }, "name": "B", "parameters": Object { "__isArgsStory": false, @@ -847,6 +931,11 @@ describe('StoryStore', () => { "foo": "c", }, "kind": "Component Two", + "moduleExport": Object { + "args": Object { + "foo": "c", + }, + }, "name": "C", "parameters": Object { "__isArgsStory": false, diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts index c1a0912469cf..08c70dfa72ef 100644 --- a/lib/store/src/StoryStore.ts +++ b/lib/store/src/StoryStore.ts @@ -121,7 +121,6 @@ export class StoryStore { if (this.cachedCSFFiles) await this.cacheAllCSFFiles(); } - // FIXME: does this need to be loaded by extract etc? async loadDocsFileById( docsId: StoryId ): Promise<{ docsExports: ModuleExports; csfFiles: CSFFile[] }> { @@ -240,28 +239,33 @@ export class StoryStore { throw new Error('Cannot call extract() unless you call cacheAllCSFFiles() first.'); } - return Object.entries(this.storyIndex.entries).reduce((acc, [storyId, { importPath }]) => { - const csfFile = this.cachedCSFFiles[importPath]; - const story = this.storyFromCSFFile({ storyId, csfFile }); - - if (!options.includeDocsOnly && story.parameters.docsOnly) { + return Object.entries(this.storyIndex.entries).reduce( + (acc, [storyId, { type, importPath }]) => { + if (type === 'docs') return acc; + + const csfFile = this.cachedCSFFiles[importPath]; + const story = this.storyFromCSFFile({ storyId, csfFile }); + + if (!options.includeDocsOnly && story.parameters.docsOnly) { + return acc; + } + + acc[storyId] = Object.entries(story).reduce( + (storyAcc, [key, value]) => { + if (typeof value === 'function') { + return storyAcc; + } + if (Array.isArray(value)) { + return Object.assign(storyAcc, { [key]: value.slice().sort() }); + } + return Object.assign(storyAcc, { [key]: value }); + }, + { args: story.initialArgs } + ); return acc; - } - - acc[storyId] = Object.entries(story).reduce( - (storyAcc, [key, value]) => { - if (typeof value === 'function') { - return storyAcc; - } - if (Array.isArray(value)) { - return Object.assign(storyAcc, { [key]: value.slice().sort() }); - } - return Object.assign(storyAcc, { [key]: value }); - }, - { args: story.initialArgs } - ); - return acc; - }, {} as Record); + }, + {} as Record + ); } getSetStoriesPayload() { diff --git a/lib/store/src/csf/normalizeStory.test.ts b/lib/store/src/csf/normalizeStory.test.ts index 6da2b1458102..3492178f87ff 100644 --- a/lib/store/src/csf/normalizeStory.test.ts +++ b/lib/store/src/csf/normalizeStory.test.ts @@ -50,6 +50,7 @@ describe('normalizeStory', () => { "decorators": Array [], "id": "title--story-export", "loaders": Array [], + "moduleExport": [Function], "name": "Story Export", "parameters": Object {}, "userStoryFn": [Function], @@ -117,10 +118,12 @@ describe('normalizeStory', () => { "decorators": Array [], "id": "title--story-export", "loaders": Array [], + "moduleExport": Object {}, "name": "Story Export", "parameters": Object {}, } `); + expect(normalized.moduleExport).toBe(storyObj); }); it('full annotations', () => { @@ -133,7 +136,7 @@ describe('normalizeStory', () => { argTypes: { storyArgType: { type: 'string' } }, }; const meta = { title: 'title' }; - const normalized = normalizeStory('storyExport', storyObj, meta); + const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta); expect(normalized).toMatchInlineSnapshot(` Object { "argTypes": Object { @@ -160,6 +163,7 @@ describe('normalizeStory', () => { }, } `); + expect(moduleExport).toBe(storyObj); }); it('prefers new annotations to legacy, but combines', () => { @@ -179,7 +183,7 @@ describe('normalizeStory', () => { }, }; const meta = { title: 'title' }; - const normalized = normalizeStory('storyExport', storyObj, meta); + const { moduleExport, ...normalized } = normalizeStory('storyExport', storyObj, meta); expect(normalized).toMatchInlineSnapshot(` Object { "argTypes": Object { @@ -216,6 +220,7 @@ describe('normalizeStory', () => { }, } `); + expect(moduleExport).toBe(storyObj); }); }); }); diff --git a/lib/store/src/csf/prepareStory.test.ts b/lib/store/src/csf/prepareStory.test.ts index 3f4a035f9322..a5005d87a339 100644 --- a/lib/store/src/csf/prepareStory.test.ts +++ b/lib/store/src/csf/prepareStory.test.ts @@ -21,6 +21,7 @@ const id = 'id'; const name = 'name'; const title = 'title'; const render = (args: any) => {}; +const moduleExport = {}; const stringType: SBScalarType = { name: 'string' }; const numberType: SBScalarType = { name: 'number' }; @@ -48,7 +49,7 @@ describe('prepareStory', () => { describe('parameters', () => { it('are combined in the right order', () => { const { parameters } = prepareStory( - { id, name, parameters: { a: 'story', nested: { z: 'story' } } }, + { id, name, parameters: { a: 'story', nested: { z: 'story' } }, moduleExport }, { id, title, @@ -79,14 +80,14 @@ describe('prepareStory', () => { }); it('sets a value even if metas do not have parameters', () => { - const { parameters } = prepareStory({ id, name }, { id, title }, { render }); + const { parameters } = prepareStory({ id, name, moduleExport }, { id, title }, { render }); expect(parameters).toEqual({ __isArgsStory: true }); }); it('does not set `__isArgsStory` if `passArgsFirst` is disabled', () => { const { parameters } = prepareStory( - { id, name, parameters: { passArgsFirst: false } }, + { id, name, parameters: { passArgsFirst: false }, moduleExport }, { id, title }, { render } ); @@ -95,7 +96,11 @@ describe('prepareStory', () => { }); it('does not set `__isArgsStory` if `render` does not take args', () => { - const { parameters } = prepareStory({ id, name }, { id, title }, { render: () => {} }); + const { parameters } = prepareStory( + { id, name, moduleExport }, + { id, title }, + { render: () => {} } + ); expect(parameters).toEqual({ __isArgsStory: false }); }); @@ -104,7 +109,7 @@ describe('prepareStory', () => { describe('args/initialArgs', () => { it('are combined in the right order', () => { const { initialArgs } = prepareStory( - { id, name, args: { a: 'story', nested: { z: 'story' } } }, + { id, name, args: { a: 'story', nested: { z: 'story' } }, moduleExport }, { id, title, @@ -135,7 +140,7 @@ describe('prepareStory', () => { it('can be overriden by `undefined`', () => { const { initialArgs } = prepareStory( - { id, name, args: { a: undefined } }, + { id, name, args: { a: undefined }, moduleExport }, { id, title, args: { a: 'component' } }, { render } ); @@ -143,7 +148,7 @@ describe('prepareStory', () => { }); it('sets a value even if metas do not have args', () => { - const { initialArgs } = prepareStory({ id, name }, { id, title }, { render }); + const { initialArgs } = prepareStory({ id, name, moduleExport }, { id, title }, { render }); expect(initialArgs).toEqual({}); }); @@ -170,6 +175,7 @@ describe('prepareStory', () => { arg5: { name: 'arg5', type: stringType }, arg6: { name: 'arg6', type: numberType, defaultValue: 0 }, // See https://github.com/storybookjs/storybook/issues/12767 } }, + moduleExport, }, { id, title }, { render: () => {} } @@ -198,7 +204,7 @@ describe('prepareStory', () => { }; prepareStory( - { id, name }, + { id, name, moduleExport }, { id, title }, { render, argsEnhancers: [enhancerOne, enhancerTwo] } ); @@ -210,7 +216,7 @@ describe('prepareStory', () => { const enhancer = jest.fn(() => ({ c: 'd' })); const { initialArgs } = prepareStory( - { id, name, args: { a: 'b' } }, + { id, name, args: { a: 'b' }, moduleExport }, { id, title }, { render, argsEnhancers: [enhancer] } ); @@ -230,7 +236,7 @@ describe('prepareStory', () => { const enhancerThree = jest.fn(() => ({ c: 'C' })); const { initialArgs } = prepareStory( - { id, name, args: { a: 'A' } }, + { id, name, args: { a: 'A' }, moduleExport }, { id, title }, { render, argsEnhancers: [enhancerOne, enhancerTwo, enhancerThree] } ); @@ -263,6 +269,7 @@ describe('prepareStory', () => { a: { name: 'a-story', type: booleanType }, nested: { name: 'nested', type: booleanType, a: 'story' }, }, + moduleExport, }, { id, @@ -295,7 +302,7 @@ describe('prepareStory', () => { it('allows you to alter argTypes when stories are added', () => { const enhancer = jest.fn((context) => ({ ...context.argTypes, c: { name: 'd' } })); const { argTypes } = prepareStory( - { id, name, argTypes: { a: { name: 'b' } } }, + { id, name, argTypes: { a: { name: 'b' } }, moduleExport }, { id, title }, { render, argTypesEnhancers: [enhancer] } ); @@ -309,7 +316,7 @@ describe('prepareStory', () => { it('does not merge argType enhancer results', () => { const enhancer = jest.fn(() => ({ c: { name: 'd' } })); const { argTypes } = prepareStory( - { id, name, argTypes: { a: { name: 'b' } } }, + { id, name, argTypes: { a: { name: 'b' } }, moduleExport }, { id, title }, { render, argTypesEnhancers: [enhancer] } ); @@ -324,7 +331,7 @@ describe('prepareStory', () => { const firstEnhancer = jest.fn((context) => ({ ...context.argTypes, c: { name: 'd' } })); const secondEnhancer = jest.fn((context) => ({ ...context.argTypes, e: { name: 'f' } })); const { argTypes } = prepareStory( - { id, name, argTypes: { a: { name: 'b' } } }, + { id, name, argTypes: { a: { name: 'b' } }, moduleExport }, { id, title }, { render, argTypesEnhancers: [firstEnhancer, secondEnhancer] } ); @@ -344,7 +351,7 @@ describe('prepareStory', () => { it('awaits the result of a loader', async () => { const loader = jest.fn(async () => new Promise((r) => setTimeout(() => r({ foo: 7 }), 100))); const { applyLoaders } = prepareStory( - { id, name, loaders: [loader] }, + { id, name, loaders: [loader], moduleExport }, { id, title }, { render } ); @@ -365,7 +372,7 @@ describe('prepareStory', () => { const storyLoader = async () => ({ foo: 5 }); const { applyLoaders } = prepareStory( - { id, name, loaders: [storyLoader] }, + { id, name, loaders: [storyLoader], moduleExport }, { id, title, loaders: [componentLoader] }, { render, loaders: [globalLoader] } ); @@ -385,7 +392,11 @@ describe('prepareStory', () => { async () => new Promise((r) => setTimeout(() => r({ foo: 3 }), 50)), ]; - const { applyLoaders } = prepareStory({ id, name, loaders }, { id, title }, { render }); + const { applyLoaders } = prepareStory( + { id, name, loaders, moduleExport }, + { id, title }, + { render } + ); const storyContext = { context: 'value' } as any; const loadedContext = await applyLoaders(storyContext); @@ -409,6 +420,7 @@ describe('prepareStory', () => { two: { name: 'two', type: { name: 'string' }, mapping: { 1: 'no match' } }, }, args: { one: 1, two: 2, three: 3 }, + moduleExport, }, { id, title }, { render: renderMock } @@ -425,7 +437,7 @@ describe('prepareStory', () => { it('passes args as the first argument to the story if `parameters.passArgsFirst` is true', () => { const renderMock = jest.fn(); const firstStory = prepareStory( - { id, name, args: { a: 1 }, parameters: { passArgsFirst: true } }, + { id, name, args: { a: 1 }, parameters: { passArgsFirst: true }, moduleExport }, { id, title }, { render: renderMock } ); @@ -437,7 +449,7 @@ describe('prepareStory', () => { ); const secondStory = prepareStory( - { id, name, args: { a: 1 }, parameters: { passArgsFirst: false } }, + { id, name, args: { a: 1 }, parameters: { passArgsFirst: false }, moduleExport }, { id, title }, { render: renderMock } ); @@ -458,6 +470,7 @@ describe('prepareStory', () => { id, name, decorators: [storyDecorator], + moduleExport, }, { id, title, decorators: [componentDecorator] }, { render: renderMock, decorators: [globalDecorator] } @@ -488,6 +501,7 @@ describe('prepareStory', () => { name, args: { a: 1, b: 2 }, argTypes: { b: { name: 'b', target: 'foo' } }, + moduleExport, }, { id, title }, { render: renderMock } @@ -512,6 +526,7 @@ describe('prepareStory', () => { name, args: { a: 1, b: 2 }, argTypes: { b: { name: 'b', if: { arg: 'a', truthy: false } } }, + moduleExport, }, { id, title }, { render: renderMock } @@ -536,6 +551,7 @@ describe('prepareStory', () => { name, args: { a: 1, b: 2 }, argTypes: { b: { name: 'b', target: 'foo' } }, + moduleExport, }, { id, title }, { render: renderMock } @@ -560,6 +576,7 @@ describe('prepareStory', () => { name, args: { b: 2 }, argTypes: { b: { name: 'b', target: 'foo' } }, + moduleExport, }, { id, title }, { render: renderMock } @@ -582,6 +599,7 @@ describe('prepareStory', () => { { id, name, + moduleExport, }, { id, title }, { render: renderMock } @@ -604,10 +622,22 @@ describe('playFunction', () => { await new Promise((r) => setTimeout(r, 0)); // Ensure this puts an async boundary in inner(); }); - const { playFunction } = prepareStory({ id, name, play }, { id, title }, { render }); + const { playFunction } = prepareStory( + { id, name, play, moduleExport }, + { id, title }, + { render } + ); await playFunction({} as StoryContext); expect(play).toHaveBeenCalled(); expect(inner).toHaveBeenCalled(); }); }); + +describe('moduleExport', () => { + it('are carried through from the story annotations', () => { + const storyObj = {}; + const story = prepareStory({ id, name, moduleExport: storyObj }, { id, title }, { render }); + expect(story.moduleExport).toBe(storyObj); + }); +}); diff --git a/lib/store/src/types.ts b/lib/store/src/types.ts index e8afb7db1757..17ca8a9a986e 100644 --- a/lib/store/src/types.ts +++ b/lib/store/src/types.ts @@ -98,7 +98,7 @@ interface BaseIndexEntry { importPath: Path; } export type StoryIndexEntry = BaseIndexEntry & { - type: 'story'; + type?: 'story'; }; export type DocsIndexEntry = BaseIndexEntry & { From 20fb7490742dbf09a668018345a904635e561abe Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 5 May 2022 17:36:23 +1000 Subject: [PATCH 05/16] Added tests and fixes in `preview-web` --- addons/docs/src/blocks/DocsRenderer.tsx | 3 - lib/preview-web/src/DocsRender.ts | 14 +- lib/preview-web/src/PreviewWeb.mockdata.ts | 44 +- lib/preview-web/src/PreviewWeb.test.ts | 491 +++++++++++++-------- lib/preview-web/src/PreviewWeb.tsx | 22 +- lib/preview-web/src/StoryRender.ts | 12 +- lib/store/src/StoryStore.ts | 49 +- 7 files changed, 393 insertions(+), 242 deletions(-) diff --git a/addons/docs/src/blocks/DocsRenderer.tsx b/addons/docs/src/blocks/DocsRenderer.tsx index c38a1c8d0e81..928f4948e069 100644 --- a/addons/docs/src/blocks/DocsRenderer.tsx +++ b/addons/docs/src/blocks/DocsRenderer.tsx @@ -24,8 +24,6 @@ async function renderDocsAsync( docsParameters: Parameters, element: HTMLElement ) { - console.log(docsParameters); - // FIXME -- use DocsContainer, make it work for modern const SimpleContainer = ({ children }: any) => ( {children} @@ -37,7 +35,6 @@ async function renderDocsAsync( (docsContext.legacy ? DocsContainer : SimpleContainer); const Page: ComponentType = docsParameters.page || (await docsParameters.getPage?.()) || DocsPage; - console.log(docsParameters.page, Page); // Use `title` as a key so that we force a re-render every time we switch components const docsElement = ( diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts index b73e4411888e..eaed9932fbd7 100644 --- a/lib/preview-web/src/DocsRender.ts +++ b/lib/preview-web/src/DocsRender.ts @@ -12,7 +12,7 @@ export class DocsRender implements Render; @@ -24,7 +24,7 @@ export class DocsRender implements Render; + private docsContext?: DocsContextProps; public disableKeyListeners = false; @@ -36,7 +36,7 @@ export class DocsRender implements Render implements Render['renderStoryToElement'] ): Promise> { const { id, title, name } = this.entry; @@ -126,13 +126,13 @@ export class DocsRender implements Render implements Render)( - this.context, + this.docsContext, { ...docs, ...(!this.legacy && { page: this.exports.default }), diff --git a/lib/preview-web/src/PreviewWeb.mockdata.ts b/lib/preview-web/src/PreviewWeb.mockdata.ts index cb3d4dc928bc..8346fc021963 100644 --- a/lib/preview-web/src/PreviewWeb.mockdata.ts +++ b/lib/preview-web/src/PreviewWeb.mockdata.ts @@ -1,7 +1,6 @@ import { EventEmitter } from 'events'; import Events from '@storybook/core-events'; import { StoryIndex } from '@storybook/store'; -import { RenderPhase } from './PreviewWeb'; export const componentOneExports = { default: { @@ -21,16 +20,34 @@ export const componentTwoExports = { default: { title: 'Component Two' }, c: { args: { foo: 'c' } }, }; -export const importFn = jest.fn(async (path) => { - return path === './src/ComponentOne.stories.js' ? componentOneExports : componentTwoExports; -}); +export const legacyDocsExports = { + default: { title: 'Introduction' }, + Docs: { parameters: { docs: { page: jest.fn() } } }, +}; +export const modernDocsExports = { + default: jest.fn(), +}; +export const importFn = jest.fn( + async (path) => + ({ + './src/ComponentOne.stories.js': componentOneExports, + './src/ComponentTwo.stories.js': componentTwoExports, + './src/Legacy.stories.mdx': legacyDocsExports, + './src/Introduction.docs.mdx': modernDocsExports, + }[path]) +); +export const docsRenderer = { + renderDocs: jest.fn().mockImplementation((context, parameters, element, cb) => cb()), + unmountDocs: jest.fn(), +}; export const projectAnnotations = { globals: { a: 'b' }, globalTypes: {}, decorators: [jest.fn((s) => s())], render: jest.fn(), renderToDOM: jest.fn(), + parameters: { docs: { renderer: () => docsRenderer } }, }; export const getProjectAnnotations = () => projectAnnotations; @@ -55,6 +72,23 @@ export const storyIndex: StoryIndex = { name: 'C', importPath: './src/ComponentTwo.stories.js', }, + 'introduction--docs': { + type: 'docs', + id: 'introduction--docs', + title: 'Introduction', + name: 'Docs', + importPath: './src/Introduction.docs.mdx', + storiesImports: ['./src/ComponentTwo.stories.js'], + }, + 'legacy--docs': { + type: 'docs', + legacy: true, + id: 'legacy--docs', + title: 'Legacy', + name: 'Docs', + importPath: './src/Legacy.stories.mdx', + storiesImports: [], + }, }, }; export const getStoryIndex = () => storyIndex; @@ -106,7 +140,7 @@ export const waitForRender = () => Events.STORY_MISSING, ]); -export const waitForRenderPhase = (phase: RenderPhase) => +export const waitForRenderPhase = (phase) => waitForEvents([Events.STORY_RENDER_PHASE_CHANGED], ({ newPhase }) => newPhase === phase); // A little trick to ensure that we always call the real `setTimeout` even when timers are mocked diff --git a/lib/preview-web/src/PreviewWeb.test.ts b/lib/preview-web/src/PreviewWeb.test.ts index a2253ab6a3ad..bd036bc55ad3 100644 --- a/lib/preview-web/src/PreviewWeb.test.ts +++ b/lib/preview-web/src/PreviewWeb.test.ts @@ -1,5 +1,4 @@ import global from 'global'; -import * as ReactDOM from 'react-dom'; import merge from 'lodash/merge'; import Events, { IGNORED_EXCEPTION } from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; @@ -21,6 +20,9 @@ import { waitForRender, waitForQuiescence, waitForRenderPhase, + docsRenderer, + legacyDocsExports, + modernDocsExports, } from './PreviewWeb.mockdata'; jest.mock('./WebView'); @@ -100,8 +102,7 @@ beforeEach(() => { projectAnnotations.renderToDOM.mockReset(); projectAnnotations.render.mockClear(); projectAnnotations.decorators[0].mockClear(); - // @ts-ignore - ReactDOM.render.mockReset().mockImplementation((_: any, _2: any, cb: () => any) => cb()); + docsRenderer.renderDocs.mockClear(); // @ts-ignore logger.warn.mockClear(); mockStoryIndex.mockReset().mockReturnValue(storyIndex); @@ -358,256 +359,307 @@ describe('PreviewWeb', () => { expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_MISSING); }); - describe('in story viewMode', () => { - it('calls view.prepareForStory', async () => { - document.location.search = '?id=component-one--a'; - - const preview = await createAndRenderPreview(); - - expect(preview.view.prepareForStory).toHaveBeenCalledWith( - expect.objectContaining({ - id: 'component-one--a', - }) - ); - }); + describe('story entries', () => { + describe('in story viewMode', () => { + it('calls view.prepareForStory', async () => { + document.location.search = '?id=component-one--a'; - it('emits STORY_PREPARED', async () => { - document.location.search = '?id=component-one--a'; - await createAndRenderPreview(); + const preview = await createAndRenderPreview(); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_PREPARED, { - id: 'component-one--a', - parameters: { - __isArgsStory: false, - docs: { container: expect.any(Function) }, - fileName: './src/ComponentOne.stories.js', - }, - initialArgs: { foo: 'a' }, - argTypes: { foo: { name: 'foo', type: { name: 'string' } } }, - args: { foo: 'a' }, + expect(preview.view.prepareForStory).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'component-one--a', + }) + ); }); - }); - it('applies loaders with story context', async () => { - document.location.search = '?id=component-one--a'; - await createAndRenderPreview(); + it('emits STORY_PREPARED', async () => { + document.location.search = '?id=component-one--a'; + await createAndRenderPreview(); - expect(componentOneExports.default.loaders[0]).toHaveBeenCalledWith( - expect.objectContaining({ + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_PREPARED, { id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, initialArgs: { foo: 'a' }, argTypes: { foo: { name: 'foo', type: { name: 'string' } } }, args: { foo: 'a' }, - }) - ); - }); + }); + }); - it('passes loaded context to renderToDOM', async () => { - document.location.search = '?id=component-one--a'; - await createAndRenderPreview(); + it('applies loaders with story context', async () => { + document.location.search = '?id=component-one--a'; + await createAndRenderPreview(); - expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith( - expect.objectContaining({ - forceRemount: true, - storyContext: expect.objectContaining({ + expect(componentOneExports.default.loaders[0]).toHaveBeenCalledWith( + expect.objectContaining({ id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, - globals: { a: 'b' }, initialArgs: { foo: 'a' }, argTypes: { foo: { name: 'foo', type: { name: 'string' } } }, args: { foo: 'a' }, - loaded: { l: 7 }, - }), - }), - undefined // this is coming from view.prepareForStory, not super important - ); - }); + }) + ); + }); - it('renders exception if a loader throws', async () => { - const error = new Error('error'); - componentOneExports.default.loaders[0].mockImplementationOnce(() => { - throw error; + it('passes loaded context to renderToDOM', async () => { + document.location.search = '?id=component-one--a'; + await createAndRenderPreview(); + + expect(projectAnnotations.renderToDOM).toHaveBeenCalledWith( + expect.objectContaining({ + forceRemount: true, + storyContext: expect.objectContaining({ + id: 'component-one--a', + parameters: { + __isArgsStory: false, + docs: expect.any(Object), + fileName: './src/ComponentOne.stories.js', + }, + globals: { a: 'b' }, + initialArgs: { foo: 'a' }, + argTypes: { foo: { name: 'foo', type: { name: 'string' } } }, + args: { foo: 'a' }, + loaded: { l: 7 }, + }), + }), + undefined // this is coming from view.prepareForStory, not super important + ); }); - document.location.search = '?id=component-one--a'; - const preview = await createAndRenderPreview(); + it('renders exception if a loader throws', async () => { + const error = new Error('error'); + componentOneExports.default.loaders[0].mockImplementationOnce(() => { + throw error; + }); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); - expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); - }); + document.location.search = '?id=component-one--a'; + const preview = await createAndRenderPreview(); - it('renders exception if renderToDOM throws', async () => { - const error = new Error('error'); - projectAnnotations.renderToDOM.mockImplementationOnce(() => { - throw error; + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); + expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); }); - document.location.search = '?id=component-one--a'; - const preview = await createAndRenderPreview(); + it('renders exception if renderToDOM throws', async () => { + const error = new Error('error'); + projectAnnotations.renderToDOM.mockImplementationOnce(() => { + throw error; + }); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); - expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); - }); + document.location.search = '?id=component-one--a'; + const preview = await createAndRenderPreview(); - it('renders helpful message if renderToDOM is undefined', async () => { - const originalRenderToDOM = projectAnnotations.renderToDOM; - try { - projectAnnotations.renderToDOM = undefined; + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); + expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); + }); - document.location.search = '?id=component-one--a'; - const preview = new PreviewWeb(); - await expect(preview.initialize({ importFn, getProjectAnnotations })).rejects.toThrow(); + it('renders helpful message if renderToDOM is undefined', async () => { + const originalRenderToDOM = projectAnnotations.renderToDOM; + try { + projectAnnotations.renderToDOM = undefined; - expect(preview.view.showErrorDisplay).toHaveBeenCalled(); - expect((preview.view.showErrorDisplay as jest.Mock).mock.calls[0][0]) - .toMatchInlineSnapshot(` + document.location.search = '?id=component-one--a'; + const preview = new PreviewWeb(); + await expect(preview.initialize({ importFn, getProjectAnnotations })).rejects.toThrow(); + + expect(preview.view.showErrorDisplay).toHaveBeenCalled(); + expect((preview.view.showErrorDisplay as jest.Mock).mock.calls[0][0]) + .toMatchInlineSnapshot(` [Error: Expected your framework's preset to export a \`renderToDOM\` field. Perhaps it needs to be upgraded for Storybook 6.4? More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field ] `); - } finally { - projectAnnotations.renderToDOM = originalRenderToDOM; - } - }); + } finally { + projectAnnotations.renderToDOM = originalRenderToDOM; + } + }); - it('renders exception if the play function throws', async () => { - const error = new Error('error'); - componentOneExports.a.play.mockImplementationOnce(() => { - throw error; + it('renders exception if the play function throws', async () => { + const error = new Error('error'); + componentOneExports.a.play.mockImplementationOnce(() => { + throw error; + }); + + document.location.search = '?id=component-one--a'; + const preview = await createAndRenderPreview(); + + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); + expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); }); - document.location.search = '?id=component-one--a'; - const preview = await createAndRenderPreview(); + it('renders error if the story calls showError', async () => { + const error = { title: 'title', description: 'description' }; + projectAnnotations.renderToDOM.mockImplementationOnce((context) => + context.showError(error) + ); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); - expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); - }); + document.location.search = '?id=component-one--a'; + const preview = await createAndRenderPreview(); - it('renders error if the story calls showError', async () => { - const error = { title: 'title', description: 'description' }; - projectAnnotations.renderToDOM.mockImplementationOnce((context) => - context.showError(error) - ); + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_ERRORED, error); + expect(preview.view.showErrorDisplay).toHaveBeenCalledWith({ + message: error.title, + stack: error.description, + }); + }); - document.location.search = '?id=component-one--a'; - const preview = await createAndRenderPreview(); + it('renders exception if the story calls showException', async () => { + const error = new Error('error'); + projectAnnotations.renderToDOM.mockImplementationOnce((context) => + context.showException(error) + ); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_ERRORED, error); - expect(preview.view.showErrorDisplay).toHaveBeenCalledWith({ - message: error.title, - stack: error.description, + document.location.search = '?id=component-one--a'; + const preview = await createAndRenderPreview(); + + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); + expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); }); - }); - it('renders exception if the story calls showException', async () => { - const error = new Error('error'); - projectAnnotations.renderToDOM.mockImplementationOnce((context) => - context.showException(error) - ); + it('executes playFunction', async () => { + document.location.search = '?id=component-one--a'; + await createAndRenderPreview(); - document.location.search = '?id=component-one--a'; - const preview = await createAndRenderPreview(); + expect(componentOneExports.a.play).toHaveBeenCalled(); + }); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_THREW_EXCEPTION, error); - expect(preview.view.showErrorDisplay).toHaveBeenCalledWith(error); - }); + it('emits STORY_RENDERED', async () => { + document.location.search = '?id=component-one--a'; + await createAndRenderPreview(); - it('executes playFunction', async () => { - document.location.search = '?id=component-one--a'; - await createAndRenderPreview(); + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_RENDERED, 'component-one--a'); + }); - expect(componentOneExports.a.play).toHaveBeenCalled(); - }); + it('does not show error display if the render function throws IGNORED_EXCEPTION', async () => { + document.location.search = '?id=component-one--a'; + projectAnnotations.renderToDOM.mockImplementationOnce(() => { + throw IGNORED_EXCEPTION; + }); - it('emits STORY_RENDERED', async () => { - document.location.search = '?id=component-one--a'; - await createAndRenderPreview(); + const preview = new PreviewWeb(); + await preview.initialize({ importFn, getProjectAnnotations }); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_RENDERED, 'component-one--a'); + await waitForRender(); + + expect(mockChannel.emit).toHaveBeenCalledWith( + Events.STORY_THREW_EXCEPTION, + IGNORED_EXCEPTION + ); + expect(preview.view.showErrorDisplay).not.toHaveBeenCalled(); + }); }); - it('does not show error display if the render function throws IGNORED_EXCEPTION', async () => { - document.location.search = '?id=component-one--a'; - projectAnnotations.renderToDOM.mockImplementationOnce(() => { - throw IGNORED_EXCEPTION; + describe('in docs viewMode', () => { + it('calls view.prepareForDocs', async () => { + document.location.search = '?id=component-one--a&viewMode=docs'; + const preview = await createAndRenderPreview(); + + expect(preview.view.prepareForDocs).toHaveBeenCalled(); }); - const preview = new PreviewWeb(); - await preview.initialize({ importFn, getProjectAnnotations }); + it('emits STORY_PREPARED', async () => { + document.location.search = '?id=component-one--a&viewMode=docs'; + await createAndRenderPreview(); - await waitForRender(); + expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_PREPARED, { + id: 'component-one--a', + parameters: { + __isArgsStory: false, + docs: expect.any(Object), + fileName: './src/ComponentOne.stories.js', + }, + initialArgs: { foo: 'a' }, + argTypes: { foo: { name: 'foo', type: { name: 'string' } } }, + args: { foo: 'a' }, + }); + }); - expect(mockChannel.emit).toHaveBeenCalledWith( - Events.STORY_THREW_EXCEPTION, - IGNORED_EXCEPTION - ); - expect(preview.view.showErrorDisplay).not.toHaveBeenCalled(); + it('calls the docs renderer with the correct context, and parameters', async () => { + document.location.search = '?id=component-one--a&viewMode=docs'; + + await createAndRenderPreview(); + + expect(docsRenderer.renderDocs).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'component-one--a', + title: 'Component One', + name: 'A', + }), + expect.objectContaining({}), // docs parameters, nothing special + 'docs-element', + expect.any(Function) + ); + }); + + it('emits DOCS_RENDERED', async () => { + document.location.search = '?id=component-one--a&viewMode=docs'; + + await createAndRenderPreview(); + + expect(mockChannel.emit).toHaveBeenCalledWith(Events.DOCS_RENDERED, 'component-one--a'); + }); }); }); - describe('in docs viewMode', () => { - it('calls view.prepareForDocs', async () => { - document.location.search = '?id=component-one--a&viewMode=docs'; - const preview = await createAndRenderPreview(); + describe('legacy docs entries', () => { + it('always renders in docs viewMode', async () => { + document.location.search = '?id=legacy--docs'; + await createAndRenderPreview(); - expect(preview.view.prepareForDocs).toHaveBeenCalled(); + expect(mockChannel.emit).toHaveBeenCalledWith(Events.DOCS_RENDERED, 'legacy--docs'); }); - - it('emits STORY_PREPARED', async () => { - document.location.search = '?id=component-one--a&viewMode=docs'; + it('renders with the generated docs parameters', async () => { + document.location.search = '?id=legacy--docs&viewMode=docs'; await createAndRenderPreview(); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.STORY_PREPARED, { - id: 'component-one--a', - parameters: { - __isArgsStory: false, - docs: { container: expect.any(Function) }, - fileName: './src/ComponentOne.stories.js', - }, - initialArgs: { foo: 'a' }, - argTypes: { foo: { name: 'foo', type: { name: 'string' } } }, - args: { foo: 'a' }, - }); + expect(docsRenderer.renderDocs).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + page: legacyDocsExports.Docs.parameters.docs.page, + renderer: projectAnnotations.parameters.docs.renderer, + }), + 'docs-element', + expect.any(Function) + ); }); + }); - it('render the docs container with the correct context', async () => { - document.location.search = '?id=component-one--a&viewMode=docs'; + describe('modern docs entries', () => { + it('always renders in docs viewMode', async () => { + document.location.search = '?id=introduction--docs'; + await createAndRenderPreview(); + expect(mockChannel.emit).toHaveBeenCalledWith(Events.DOCS_RENDERED, 'introduction--docs'); + }); + it('renders with the generated docs parameters', async () => { + document.location.search = '?id=introduction--docs&viewMode=docs'; await createAndRenderPreview(); - expect(ReactDOM.render).toHaveBeenCalledWith( + expect(docsRenderer.renderDocs).toHaveBeenCalledWith( + expect.any(Object), expect.objectContaining({ - type: componentOneExports.default.parameters.docs.container, - props: expect.objectContaining({ - context: expect.objectContaining({ - id: 'component-one--a', - title: 'Component One', - name: 'A', - }), - }), + page: modernDocsExports.default, + renderer: projectAnnotations.parameters.docs.renderer, }), 'docs-element', expect.any(Function) ); }); - it('emits DOCS_RENDERED', async () => { - document.location.search = '?id=component-one--a&viewMode=docs'; - + it('loads imports of the docs entry', async () => { + document.location.search = '?id=introduction--docs'; await createAndRenderPreview(); - expect(mockChannel.emit).toHaveBeenCalledWith(Events.DOCS_RENDERED, 'component-one--a'); + expect(importFn).toHaveBeenCalledWith('./src/ComponentTwo.stories.js'); }); }); }); @@ -676,7 +728,7 @@ describe('PreviewWeb', () => { emitter.emit(Events.UPDATE_GLOBALS, { globals: { foo: 'bar' } }); await waitForRender(); - expect(ReactDOM.render).toHaveBeenCalledTimes(2); + expect(docsRenderer.renderDocs).toHaveBeenCalledTimes(2); }); }); }); @@ -945,7 +997,7 @@ describe('PreviewWeb', () => { await createAndRenderPreview(); - (ReactDOM.render as jest.MockedFunction).mockClear(); + docsRenderer.renderDocs.mockClear(); mockChannel.emit.mockClear(); emitter.emit(Events.UPDATE_STORY_ARGS, { storyId: 'component-one--a', @@ -953,7 +1005,7 @@ describe('PreviewWeb', () => { }); await waitForRender(); - expect(ReactDOM.render).toHaveBeenCalledTimes(1); + expect(docsRenderer.renderDocs).toHaveBeenCalledTimes(1); }); }); @@ -969,7 +1021,7 @@ describe('PreviewWeb', () => { await createAndRenderPreview(); - (ReactDOM.render as jest.MockedFunction).mockClear(); + docsRenderer.renderDocs.mockClear(); mockChannel.emit.mockClear(); emitter.emit(Events.UPDATE_STORY_ARGS, { storyId: 'component-one--a', @@ -977,7 +1029,7 @@ describe('PreviewWeb', () => { }); await waitForEvents([Events.STORY_ARGS_UPDATED]); - expect(ReactDOM.render).not.toHaveBeenCalled(); + expect(docsRenderer.renderDocs).not.toHaveBeenCalled(); }); describe('when renderStoryToElement was called', () => { @@ -1001,7 +1053,7 @@ describe('PreviewWeb', () => { 'story-element' ); - (ReactDOM.render as jest.MockedFunction).mockClear(); + docsRenderer.renderDocs.mockClear(); mockChannel.emit.mockClear(); emitter.emit(Events.UPDATE_STORY_ARGS, { storyId: 'component-one--a', @@ -1032,6 +1084,35 @@ describe('PreviewWeb', () => { await preview.onPreloadStories(['component-two--c']); expect(importFn).toHaveBeenCalledWith('./src/ComponentTwo.stories.js'); }); + + it('loads legacy docs entries', async () => { + document.location.search = '?id=component-one--a&viewMode=docs'; + const preview = await createAndRenderPreview(); + await waitForRender(); + + importFn.mockClear(); + await preview.onPreloadStories(['legacy--docs']); + expect(importFn).toHaveBeenCalledWith('./src/Legacy.stories.mdx'); + }); + + it('loads modern docs entries', async () => { + document.location.search = '?id=component-one--a&viewMode=docs'; + const preview = await createAndRenderPreview(); + await waitForRender(); + + importFn.mockClear(); + await preview.onPreloadStories(['introduction--docs']); + expect(importFn).toHaveBeenCalledWith('./src/Introduction.docs.mdx'); + }); + it('loads imports of modern docs entries', async () => { + document.location.search = '?id=component-one--a&viewMode=docs'; + const preview = await createAndRenderPreview(); + await waitForRender(); + + importFn.mockClear(); + await preview.onPreloadStories(['introduction--docs']); + expect(importFn).toHaveBeenCalledWith('./src/ComponentTwo.stories.js'); + }); }); describe('onResetArgs', () => { @@ -1440,7 +1521,7 @@ describe('PreviewWeb', () => { id: 'component-one--b', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, initialArgs: { foo: 'b' }, @@ -1466,7 +1547,7 @@ describe('PreviewWeb', () => { id: 'component-one--b', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, initialArgs: { foo: 'b' }, @@ -1495,7 +1576,7 @@ describe('PreviewWeb', () => { id: 'component-one--b', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, globals: { a: 'b' }, @@ -1887,17 +1968,13 @@ describe('PreviewWeb', () => { await waitForSetCurrentStory(); await waitForRender(); - expect(ReactDOM.render).toHaveBeenCalledWith( + expect(docsRenderer.renderDocs).toHaveBeenCalledWith( expect.objectContaining({ - type: componentOneExports.default.parameters.docs.container, - props: expect.objectContaining({ - context: expect.objectContaining({ - id: 'component-one--a', - title: 'Component One', - name: 'A', - }), - }), + id: 'component-one--a', + title: 'Component One', + name: 'A', }), + expect.any(Object), 'docs-element', expect.any(Function) ); @@ -1949,7 +2026,7 @@ describe('PreviewWeb', () => { await waitForSetCurrentStory(); await waitForRender(); - expect(ReactDOM.unmountComponentAtNode).toHaveBeenCalled(); + expect(docsRenderer.unmountDocs).toHaveBeenCalled(); }); // NOTE: I am not sure this entirely makes sense but this is the behaviour from 6.3 @@ -2003,7 +2080,7 @@ describe('PreviewWeb', () => { id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, initialArgs: { foo: 'a' }, @@ -2029,7 +2106,7 @@ describe('PreviewWeb', () => { id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, initialArgs: { foo: 'a' }, @@ -2058,7 +2135,7 @@ describe('PreviewWeb', () => { id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, globals: { a: 'b' }, @@ -2262,7 +2339,7 @@ describe('PreviewWeb', () => { id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, initialArgs: { foo: 'edited' }, @@ -2299,7 +2376,7 @@ describe('PreviewWeb', () => { id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, initialArgs: { foo: 'edited' }, @@ -2325,7 +2402,7 @@ describe('PreviewWeb', () => { id: 'component-one--a', parameters: { __isArgsStory: false, - docs: { container: expect.any(Function) }, + docs: expect.any(Object), fileName: './src/ComponentOne.stories.js', }, globals: { a: 'b' }, @@ -2936,11 +3013,18 @@ describe('PreviewWeb', () => { "foo": "a", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "a", + }, + "play": [MockFunction], + }, "name": "A", "parameters": Object { "__isArgsStory": false, "docs": Object { "container": [MockFunction], + "renderer": [Function], }, "fileName": "./src/ComponentOne.stories.js", }, @@ -2967,11 +3051,18 @@ describe('PreviewWeb', () => { "foo": "b", }, "kind": "Component One", + "moduleExport": Object { + "args": Object { + "foo": "b", + }, + "play": [MockFunction], + }, "name": "B", "parameters": Object { "__isArgsStory": false, "docs": Object { "container": [MockFunction], + "renderer": [Function], }, "fileName": "./src/ComponentOne.stories.js", }, @@ -2998,9 +3089,17 @@ describe('PreviewWeb', () => { "foo": "c", }, "kind": "Component Two", + "moduleExport": Object { + "args": Object { + "foo": "c", + }, + }, "name": "C", "parameters": Object { "__isArgsStory": false, + "docs": Object { + "renderer": [Function], + }, "fileName": "./src/ComponentTwo.stories.js", }, "playFunction": undefined, diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx index c5feb60109d8..c85e93a8f69b 100644 --- a/lib/preview-web/src/PreviewWeb.tsx +++ b/lib/preview-web/src/PreviewWeb.tsx @@ -210,7 +210,7 @@ export class PreviewWeb extends Preview this.storyStore.loadStory({ storyId: id }))); + await Promise.all(ids.map((id) => this.storyStore.loadEntry(id))); } // RENDERING @@ -227,11 +227,16 @@ export class PreviewWeb extends Preview extends Preview extends Preview - ).context(); + if (entry.type !== 'docs') { + const { parameters, initialArgs, argTypes, args } = this.storyStore.getStoryContext( + render.story + ); + if (global.FEATURES?.storyStoreV7) { this.channel.emit(Events.STORY_PREPARED, { id: storyId, diff --git a/lib/preview-web/src/StoryRender.ts b/lib/preview-web/src/StoryRender.ts index bbcafc70a592..1f8a7b111f80 100644 --- a/lib/preview-web/src/StoryRender.ts +++ b/lib/preview-web/src/StoryRender.ts @@ -125,10 +125,6 @@ export class StoryRender implements Render implements Render implements Render; await this.runPhase(abortSignal, 'loading', async () => { loadedContext = await applyLoaders({ - ...this.context(), + ...this.storyContext(), viewMode: this.viewMode, } as StoryContextForLoaders); }); @@ -176,7 +176,7 @@ export class StoryRender implements Render { if (this.cachedCSFFiles) await this.cacheAllCSFFiles(); } - async loadDocsFileById( - docsId: StoryId - ): Promise<{ docsExports: ModuleExports; csfFiles: CSFFile[] }> { - const entry = this.storyIndex.storyIdToEntry(docsId); - if (entry.type !== 'docs') throw new Error(`Cannot load docs file for id ${docsId}`); - - const { importPath, storiesImports } = entry; - - const [docsExports, ...csfFiles] = (await Promise.all([ - this.importFn(importPath), - ...storiesImports.map((storyImportPath) => { - const firstStoryEntry = this.storyIndex.importPathToEntry(storyImportPath); - return this.loadCSFFileByStoryId(firstStoryEntry.id); - }), - ])) as [ModuleExports, ...CSFFile[]]; - - return { docsExports, csfFiles }; + // Get an entry from the index, waiting on initialization if necessary + async storyIdToEntry(storyId: StoryId) { + await this.initializationPromise; + return this.storyIndex.storyIdToEntry(storyId); } // To load a single CSF file to service a story we need to look up the importPath in the index @@ -217,6 +205,33 @@ export class StoryStore { .map((storyId: StoryId) => this.storyFromCSFFile({ storyId, csfFile })); } + async loadDocsFileById( + docsId: StoryId + ): Promise<{ docsExports: ModuleExports; csfFiles: CSFFile[] }> { + const entry = await this.storyIdToEntry(docsId); + if (entry.type !== 'docs') throw new Error(`Cannot load docs file for id ${docsId}`); + + const { importPath, storiesImports } = entry; + + const [docsExports, ...csfFiles] = (await Promise.all([ + this.importFn(importPath), + ...storiesImports.map((storyImportPath) => { + const firstStoryEntry = this.storyIndex.importPathToEntry(storyImportPath); + return this.loadCSFFileByStoryId(firstStoryEntry.id); + }), + ])) as [ModuleExports, ...CSFFile[]]; + + return { docsExports, csfFiles }; + } + + async loadEntry(id: StoryId) { + const entry = await this.storyIdToEntry(id); + if (entry.type === 'docs' && !entry.legacy) { + return this.loadDocsFileById(id); + } + return this.loadCSFFileByStoryId(id); + } + // A prepared story does not include args, globals or hooks. These are stored in the story store // and updated separtely to the (immutable) story. getStoryContext(story: Story): Omit, 'viewMode'> { From 693bd0556576370c073090e1839c21496c3056db Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 5 May 2022 17:53:22 +1000 Subject: [PATCH 06/16] Clean up a little bit --- addons/docs/src/blocks/DocsRenderer.tsx | 4 --- addons/docs/src/blocks/NoDocs.tsx | 39 ------------------------- 2 files changed, 43 deletions(-) delete mode 100644 addons/docs/src/blocks/NoDocs.tsx diff --git a/addons/docs/src/blocks/DocsRenderer.tsx b/addons/docs/src/blocks/DocsRenderer.tsx index 928f4948e069..917ec49b7d02 100644 --- a/addons/docs/src/blocks/DocsRenderer.tsx +++ b/addons/docs/src/blocks/DocsRenderer.tsx @@ -1,15 +1,11 @@ import React, { ComponentType, ReactElement } from 'react'; import ReactDOM from 'react-dom'; import { AnyFramework, Parameters } from '@storybook/csf'; -import { ModuleExports, Story } from '@storybook/store'; -import type { DocsRenderFunction } from '@storybook/preview-web'; import { DocsContainer } from './DocsContainer'; import { DocsPage } from './DocsPage'; import { DocsContext, DocsContextProps } from './DocsContext'; -import { NoDocs } from './NoDocs'; -// FIXME -- make this: DocsRenderFunction export function renderDocs( docsContext: DocsContextProps, docsParameters: Parameters, diff --git a/addons/docs/src/blocks/NoDocs.tsx b/addons/docs/src/blocks/NoDocs.tsx deleted file mode 100644 index b6932363d1ac..000000000000 --- a/addons/docs/src/blocks/NoDocs.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { CSSProperties } from 'react'; - -const wrapper = { - fontSize: '14px', - letterSpacing: '0.2px', - margin: '10px 0', -}; - -const main = { - margin: 'auto', - padding: 30, - borderRadius: 10, - background: 'rgba(0,0,0,0.03)', -}; - -const heading: CSSProperties = { - textAlign: 'center', -}; - -export const NoDocs = () => ( -
-
-

No Docs

-

- Sorry, but there are no docs for the selected story. To add them, set the story's  - docs parameter. If you think this is an error: -

-
    -
  • Please check the story definition.
  • -
  • Please check the Storybook config.
  • -
  • Try reloading the page.
  • -
-

- If the problem persists, check the browser console, or the terminal you've run Storybook - from. -

-
-
-); From dfadf6a3c3b50e5caf0e93b19e49f6c9255bd299 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 5 May 2022 18:06:08 +1000 Subject: [PATCH 07/16] Fix integration tests --- addons/docs/src/blocks/DocsRenderer.tsx | 33 ++++++++++++------- addons/docs/src/blocks/index.ts | 1 + addons/docs/src/preview.ts | 5 ++- lib/preview-web/src/DocsRender.ts | 5 ++- .../src/PreviewWeb.integration.test.ts | 3 ++ lib/preview-web/src/PreviewWeb.mockdata.ts | 4 +-- lib/preview-web/src/PreviewWeb.test.ts | 24 +++++++------- 7 files changed, 43 insertions(+), 32 deletions(-) diff --git a/addons/docs/src/blocks/DocsRenderer.tsx b/addons/docs/src/blocks/DocsRenderer.tsx index 917ec49b7d02..66fa711b2850 100644 --- a/addons/docs/src/blocks/DocsRenderer.tsx +++ b/addons/docs/src/blocks/DocsRenderer.tsx @@ -1,18 +1,31 @@ import React, { ComponentType, ReactElement } from 'react'; import ReactDOM from 'react-dom'; import { AnyFramework, Parameters } from '@storybook/csf'; +import { DocsRenderFunction } from '@storybook/preview-web'; + import { DocsContainer } from './DocsContainer'; import { DocsPage } from './DocsPage'; - import { DocsContext, DocsContextProps } from './DocsContext'; -export function renderDocs( - docsContext: DocsContextProps, - docsParameters: Parameters, - element: HTMLElement, - callback: () => void -): void { - renderDocsAsync(docsContext, docsParameters, element).then(callback); +export class DocsRenderer { + public render: DocsRenderFunction; + + public unmount: (element: HTMLElement) => void; + + constructor() { + this.render = ( + docsContext: DocsContextProps, + docsParameters: Parameters, + element: HTMLElement, + callback: () => void + ): void => { + renderDocsAsync(docsContext, docsParameters, element).then(callback); + }; + + this.unmount = (element: HTMLElement) => { + ReactDOM.unmountComponentAtNode(element); + }; + } } async function renderDocsAsync( @@ -43,7 +56,3 @@ async function renderDocsAsync( ReactDOM.render(docsElement, element, resolve); }); } - -export function unmountDocs(element: HTMLElement) { - ReactDOM.unmountComponentAtNode(element); -} diff --git a/addons/docs/src/blocks/index.ts b/addons/docs/src/blocks/index.ts index 7096967b0db3..11fc1de42df1 100644 --- a/addons/docs/src/blocks/index.ts +++ b/addons/docs/src/blocks/index.ts @@ -7,6 +7,7 @@ export * from './Description'; export * from './DocsContext'; export * from './DocsPage'; export * from './DocsContainer'; +export * from './DocsRenderer'; // For testing export * from './DocsStory'; export * from './Heading'; export * from './Meta'; diff --git a/addons/docs/src/preview.ts b/addons/docs/src/preview.ts index 00ed5271d263..1cb1b297f323 100644 --- a/addons/docs/src/preview.ts +++ b/addons/docs/src/preview.ts @@ -1,9 +1,8 @@ export const parameters = { docs: { renderer: async () => { - const x = await import('./blocks/DocsRenderer'); - console.log(x); - return x; + const { DocsRenderer } = await import('./blocks/DocsRenderer'); + return new DocsRenderer(); }, }, }; diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts index eaed9932fbd7..069ce6ca21ba 100644 --- a/lib/preview-web/src/DocsRender.ts +++ b/lib/preview-web/src/DocsRender.ts @@ -144,7 +144,7 @@ export class DocsRender implements Render)( + (renderer.render as DocsRenderFunction)( this.docsContext, { ...docs, @@ -155,8 +155,7 @@ export class DocsRender implements Render { if (!viewModeChanged || !this.canvasElement) return; - // TODO type - renderer.unmountDocs(this.canvasElement); + renderer.unmount(this.canvasElement); }; } diff --git a/lib/preview-web/src/PreviewWeb.integration.test.ts b/lib/preview-web/src/PreviewWeb.integration.test.ts index 2e452c167c45..3dc66a8f8c2f 100644 --- a/lib/preview-web/src/PreviewWeb.integration.test.ts +++ b/lib/preview-web/src/PreviewWeb.integration.test.ts @@ -2,6 +2,7 @@ import React from 'react'; import global from 'global'; import { RenderContext } from '@storybook/store'; import addons, { mockChannel as createMockChannel } from '@storybook/addons'; +import { DocsRenderer } from '@storybook/addon-docs'; import { PreviewWeb } from './PreviewWeb'; import { @@ -13,6 +14,7 @@ import { mockChannel, waitForRender, storyIndex as mockStoryIndex, + docsRenderer, } from './PreviewWeb.mockdata'; // PreviewWeb.test mocks out all rendering @@ -51,6 +53,7 @@ beforeEach(() => { projectAnnotations.renderToDOM.mockReset(); projectAnnotations.render.mockClear(); projectAnnotations.decorators[0].mockClear(); + projectAnnotations.parameters.docs.renderer = () => new DocsRenderer(); addons.setChannel(mockChannel as any); addons.setServerChannel(createMockChannel()); diff --git a/lib/preview-web/src/PreviewWeb.mockdata.ts b/lib/preview-web/src/PreviewWeb.mockdata.ts index 8346fc021963..23384cb3d0f9 100644 --- a/lib/preview-web/src/PreviewWeb.mockdata.ts +++ b/lib/preview-web/src/PreviewWeb.mockdata.ts @@ -38,8 +38,8 @@ export const importFn = jest.fn( ); export const docsRenderer = { - renderDocs: jest.fn().mockImplementation((context, parameters, element, cb) => cb()), - unmountDocs: jest.fn(), + render: jest.fn().mockImplementation((context, parameters, element, cb) => cb()), + unmount: jest.fn(), }; export const projectAnnotations = { globals: { a: 'b' }, diff --git a/lib/preview-web/src/PreviewWeb.test.ts b/lib/preview-web/src/PreviewWeb.test.ts index bd036bc55ad3..a567bb84319c 100644 --- a/lib/preview-web/src/PreviewWeb.test.ts +++ b/lib/preview-web/src/PreviewWeb.test.ts @@ -102,7 +102,7 @@ beforeEach(() => { projectAnnotations.renderToDOM.mockReset(); projectAnnotations.render.mockClear(); projectAnnotations.decorators[0].mockClear(); - docsRenderer.renderDocs.mockClear(); + docsRenderer.render.mockClear(); // @ts-ignore logger.warn.mockClear(); mockStoryIndex.mockReset().mockReturnValue(storyIndex); @@ -588,7 +588,7 @@ describe('PreviewWeb', () => { await createAndRenderPreview(); - expect(docsRenderer.renderDocs).toHaveBeenCalledWith( + expect(docsRenderer.render).toHaveBeenCalledWith( expect.objectContaining({ id: 'component-one--a', title: 'Component One', @@ -621,7 +621,7 @@ describe('PreviewWeb', () => { document.location.search = '?id=legacy--docs&viewMode=docs'; await createAndRenderPreview(); - expect(docsRenderer.renderDocs).toHaveBeenCalledWith( + expect(docsRenderer.render).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ page: legacyDocsExports.Docs.parameters.docs.page, @@ -644,7 +644,7 @@ describe('PreviewWeb', () => { document.location.search = '?id=introduction--docs&viewMode=docs'; await createAndRenderPreview(); - expect(docsRenderer.renderDocs).toHaveBeenCalledWith( + expect(docsRenderer.render).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ page: modernDocsExports.default, @@ -728,7 +728,7 @@ describe('PreviewWeb', () => { emitter.emit(Events.UPDATE_GLOBALS, { globals: { foo: 'bar' } }); await waitForRender(); - expect(docsRenderer.renderDocs).toHaveBeenCalledTimes(2); + expect(docsRenderer.render).toHaveBeenCalledTimes(2); }); }); }); @@ -997,7 +997,7 @@ describe('PreviewWeb', () => { await createAndRenderPreview(); - docsRenderer.renderDocs.mockClear(); + docsRenderer.render.mockClear(); mockChannel.emit.mockClear(); emitter.emit(Events.UPDATE_STORY_ARGS, { storyId: 'component-one--a', @@ -1005,7 +1005,7 @@ describe('PreviewWeb', () => { }); await waitForRender(); - expect(docsRenderer.renderDocs).toHaveBeenCalledTimes(1); + expect(docsRenderer.render).toHaveBeenCalledTimes(1); }); }); @@ -1021,7 +1021,7 @@ describe('PreviewWeb', () => { await createAndRenderPreview(); - docsRenderer.renderDocs.mockClear(); + docsRenderer.render.mockClear(); mockChannel.emit.mockClear(); emitter.emit(Events.UPDATE_STORY_ARGS, { storyId: 'component-one--a', @@ -1029,7 +1029,7 @@ describe('PreviewWeb', () => { }); await waitForEvents([Events.STORY_ARGS_UPDATED]); - expect(docsRenderer.renderDocs).not.toHaveBeenCalled(); + expect(docsRenderer.render).not.toHaveBeenCalled(); }); describe('when renderStoryToElement was called', () => { @@ -1053,7 +1053,7 @@ describe('PreviewWeb', () => { 'story-element' ); - docsRenderer.renderDocs.mockClear(); + docsRenderer.render.mockClear(); mockChannel.emit.mockClear(); emitter.emit(Events.UPDATE_STORY_ARGS, { storyId: 'component-one--a', @@ -1968,7 +1968,7 @@ describe('PreviewWeb', () => { await waitForSetCurrentStory(); await waitForRender(); - expect(docsRenderer.renderDocs).toHaveBeenCalledWith( + expect(docsRenderer.render).toHaveBeenCalledWith( expect.objectContaining({ id: 'component-one--a', title: 'Component One', @@ -2026,7 +2026,7 @@ describe('PreviewWeb', () => { await waitForSetCurrentStory(); await waitForRender(); - expect(docsRenderer.unmountDocs).toHaveBeenCalled(); + expect(docsRenderer.unmount).toHaveBeenCalled(); }); // NOTE: I am not sure this entirely makes sense but this is the behaviour from 6.3 From 93705daa9f5ddf8c28d9d2a8b10ca659fbd2339b Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 5 May 2022 21:03:48 +1000 Subject: [PATCH 08/16] Cleanup --- lib/store/src/StoryStore.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts index 148512b0cb7a..5001ad5dad59 100644 --- a/lib/store/src/StoryStore.ts +++ b/lib/store/src/StoryStore.ts @@ -267,6 +267,7 @@ export class StoryStore { acc[storyId] = Object.entries(story).reduce( (storyAcc, [key, value]) => { + if (key === 'moduleExport') return storyAcc; if (typeof value === 'function') { return storyAcc; } From 7385dd807089814a6e02a66c78a6212e9c66c3db Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 6 May 2022 14:54:40 +1000 Subject: [PATCH 09/16] Fix deepscan problems --- addons/docs/src/blocks/DocsRenderer.tsx | 2 +- lib/preview-web/src/DocsRender.ts | 3 ++- lib/preview-web/src/PreviewWeb.integration.test.ts | 1 - lib/store/src/StoryStore.ts | 1 - lib/store/src/csf/processCSFFile.ts | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/addons/docs/src/blocks/DocsRenderer.tsx b/addons/docs/src/blocks/DocsRenderer.tsx index 66fa711b2850..b9a217a344a9 100644 --- a/addons/docs/src/blocks/DocsRenderer.tsx +++ b/addons/docs/src/blocks/DocsRenderer.tsx @@ -1,4 +1,4 @@ -import React, { ComponentType, ReactElement } from 'react'; +import React, { ComponentType } from 'react'; import ReactDOM from 'react-dom'; import { AnyFramework, Parameters } from '@storybook/csf'; import { DocsRenderFunction } from '@storybook/preview-web'; diff --git a/lib/preview-web/src/DocsRender.ts b/lib/preview-web/src/DocsRender.ts index 069ce6ca21ba..1f5c8132dce5 100644 --- a/lib/preview-web/src/DocsRender.ts +++ b/lib/preview-web/src/DocsRender.ts @@ -41,7 +41,8 @@ export class DocsRender implements Render) { - return other && this.id === other.id && this.legacy + if (!other) return false; + return this.id === other.id && this.legacy ? this.story && this.story === other.story : other.type === 'docs' && this.entry === (other as DocsRender).entry; } diff --git a/lib/preview-web/src/PreviewWeb.integration.test.ts b/lib/preview-web/src/PreviewWeb.integration.test.ts index 3dc66a8f8c2f..97715ff4a7b0 100644 --- a/lib/preview-web/src/PreviewWeb.integration.test.ts +++ b/lib/preview-web/src/PreviewWeb.integration.test.ts @@ -14,7 +14,6 @@ import { mockChannel, waitForRender, storyIndex as mockStoryIndex, - docsRenderer, } from './PreviewWeb.mockdata'; // PreviewWeb.test mocks out all rendering diff --git a/lib/store/src/StoryStore.ts b/lib/store/src/StoryStore.ts index 5001ad5dad59..1915c7139bcc 100644 --- a/lib/store/src/StoryStore.ts +++ b/lib/store/src/StoryStore.ts @@ -32,7 +32,6 @@ import type { V2CompatIndexEntry, StoryIndexV3, ModuleExports, - IndexEntry, } from './types'; import { HooksContext } from './hooks'; diff --git a/lib/store/src/csf/processCSFFile.ts b/lib/store/src/csf/processCSFFile.ts index fd0bad3afacc..02bf81c918cd 100644 --- a/lib/store/src/csf/processCSFFile.ts +++ b/lib/store/src/csf/processCSFFile.ts @@ -1,4 +1,4 @@ -import type { Parameters, AnyFramework, ComponentTitle, StoryId } from '@storybook/csf'; +import type { Parameters, AnyFramework, ComponentTitle } from '@storybook/csf'; import { isExportStory } from '@storybook/csf'; import { logger } from '@storybook/client-logger'; From 9e55afe2c7793952adc21aa8ffda495bf12d6d09 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Fri, 6 May 2022 15:08:06 +1000 Subject: [PATCH 10/16] Update snapshots --- lib/preview-web/src/PreviewWeb.test.ts | 25 +++--------------- lib/store/src/StoryStore.test.ts | 35 -------------------------- 2 files changed, 4 insertions(+), 56 deletions(-) diff --git a/lib/preview-web/src/PreviewWeb.test.ts b/lib/preview-web/src/PreviewWeb.test.ts index a567bb84319c..ef52ab5d4eea 100644 --- a/lib/preview-web/src/PreviewWeb.test.ts +++ b/lib/preview-web/src/PreviewWeb.test.ts @@ -472,12 +472,12 @@ describe('PreviewWeb', () => { expect(preview.view.showErrorDisplay).toHaveBeenCalled(); expect((preview.view.showErrorDisplay as jest.Mock).mock.calls[0][0]) .toMatchInlineSnapshot(` - [Error: Expected your framework's preset to export a \`renderToDOM\` field. + [Error: Expected your framework's preset to export a \`renderToDOM\` field. - Perhaps it needs to be upgraded for Storybook 6.4? + Perhaps it needs to be upgraded for Storybook 6.4? - More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field ] - `); + More info: https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#mainjs-framework-field ] + `); } finally { projectAnnotations.renderToDOM = originalRenderToDOM; } @@ -3013,12 +3013,6 @@ describe('PreviewWeb', () => { "foo": "a", }, "kind": "Component One", - "moduleExport": Object { - "args": Object { - "foo": "a", - }, - "play": [MockFunction], - }, "name": "A", "parameters": Object { "__isArgsStory": false, @@ -3051,12 +3045,6 @@ describe('PreviewWeb', () => { "foo": "b", }, "kind": "Component One", - "moduleExport": Object { - "args": Object { - "foo": "b", - }, - "play": [MockFunction], - }, "name": "B", "parameters": Object { "__isArgsStory": false, @@ -3089,11 +3077,6 @@ describe('PreviewWeb', () => { "foo": "c", }, "kind": "Component Two", - "moduleExport": Object { - "args": Object { - "foo": "c", - }, - }, "name": "C", "parameters": Object { "__isArgsStory": false, diff --git a/lib/store/src/StoryStore.test.ts b/lib/store/src/StoryStore.test.ts index b8e73e0e5e1a..fc844987e42d 100644 --- a/lib/store/src/StoryStore.test.ts +++ b/lib/store/src/StoryStore.test.ts @@ -332,11 +332,6 @@ describe('StoryStore', () => { "foo": "a", }, "kind": "Component One", - "moduleExport": Object { - "args": Object { - "foo": "a", - }, - }, "name": "A", "parameters": Object { "__isArgsStory": false, @@ -504,11 +499,6 @@ describe('StoryStore', () => { "foo": "a", }, "kind": "Component One", - "moduleExport": Object { - "args": Object { - "foo": "a", - }, - }, "name": "A", "parameters": Object { "__isArgsStory": false, @@ -544,11 +534,6 @@ describe('StoryStore', () => { "foo": "b", }, "kind": "Component One", - "moduleExport": Object { - "args": Object { - "foo": "b", - }, - }, "name": "B", "parameters": Object { "__isArgsStory": false, @@ -584,11 +569,6 @@ describe('StoryStore', () => { "foo": "c", }, "kind": "Component Two", - "moduleExport": Object { - "args": Object { - "foo": "c", - }, - }, "name": "C", "parameters": Object { "__isArgsStory": false, @@ -851,11 +831,6 @@ describe('StoryStore', () => { "foo": "a", }, "kind": "Component One", - "moduleExport": Object { - "args": Object { - "foo": "a", - }, - }, "name": "A", "parameters": Object { "__isArgsStory": false, @@ -891,11 +866,6 @@ describe('StoryStore', () => { "foo": "b", }, "kind": "Component One", - "moduleExport": Object { - "args": Object { - "foo": "b", - }, - }, "name": "B", "parameters": Object { "__isArgsStory": false, @@ -931,11 +901,6 @@ describe('StoryStore', () => { "foo": "c", }, "kind": "Component Two", - "moduleExport": Object { - "args": Object { - "foo": "c", - }, - }, "name": "C", "parameters": Object { "__isArgsStory": false, From 443ecdf4f294a0a5d6be083d7b6fe20a09ca1da5 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 9 May 2022 10:23:28 +1000 Subject: [PATCH 11/16] Remove accidentally added nextra demo --- examples/nextra-demo | 1 - 1 file changed, 1 deletion(-) delete mode 160000 examples/nextra-demo diff --git a/examples/nextra-demo b/examples/nextra-demo deleted file mode 160000 index 8d6b70bfdaf2..000000000000 --- a/examples/nextra-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8d6b70bfdaf25998a9b1d1fd4d2498948fd88a0e From 1e5bd78b0f158179784ef119f0dd8b08451aedbe Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 9 May 2022 10:39:28 +1000 Subject: [PATCH 12/16] Fix tests with some proper waits --- lib/core-client/src/preview/start.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/core-client/src/preview/start.test.ts b/lib/core-client/src/preview/start.test.ts index 7b6d60121091..631ce8c4f317 100644 --- a/lib/core-client/src/preview/start.test.ts +++ b/lib/core-client/src/preview/start.test.ts @@ -945,6 +945,7 @@ describe('start', () => { "v": 2, } `); + await waitForRender(); mockChannel.emit.mockClear(); disposeCallback(module.hot.data); @@ -1340,6 +1341,8 @@ describe('start', () => { "v": 2, } `); + + await waitForRender(); }); }); }); From eb00b30776967f7b93347c9424794265fd5655a4 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 9 May 2022 15:30:31 +1000 Subject: [PATCH 13/16] Fix storyshots problem with `react-ts` --- scripts/jest.init.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/jest.init.ts b/scripts/jest.init.ts index d7e36661cb69..d48d1a7bcb1a 100644 --- a/scripts/jest.init.ts +++ b/scripts/jest.init.ts @@ -44,6 +44,8 @@ const ignoreList = [ (error: any) => error.message.includes('react-async-component-lifecycle-hooks') && error.stack.includes('addons/knobs/src/components/__tests__/Options.js'), + // Storyshots blows up if your project includes a .docs.mdx file (react-ts does). + (error: any) => error.message.match(/Unexpected error while loading .*\.docs\.mdx/), ]; const throwMessage = (type: any, message: any) => { From b5e756819790b2d79fe8d16f31361db0fdcf0029 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 9 May 2022 15:35:12 +1000 Subject: [PATCH 14/16] Default `viewMode` to `story` --- lib/preview-web/src/PreviewWeb.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx index c85e93a8f69b..aebe68cfc939 100644 --- a/lib/preview-web/src/PreviewWeb.tsx +++ b/lib/preview-web/src/PreviewWeb.tsx @@ -236,7 +236,8 @@ export class PreviewWeb extends Preview Date: Mon, 9 May 2022 16:14:33 +1000 Subject: [PATCH 15/16] Exit early if entry is not found --- lib/preview-web/src/PreviewWeb.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/preview-web/src/PreviewWeb.tsx b/lib/preview-web/src/PreviewWeb.tsx index aebe68cfc939..131fbde32be8 100644 --- a/lib/preview-web/src/PreviewWeb.tsx +++ b/lib/preview-web/src/PreviewWeb.tsx @@ -230,14 +230,14 @@ export class PreviewWeb extends Preview Date: Mon, 9 May 2022 16:19:13 +1000 Subject: [PATCH 16/16] Enfore `type` on `StoryIndex` in preview --- lib/preview-web/src/PreviewWeb.integration.test.ts | 2 +- lib/preview-web/src/PreviewWeb.mockdata.ts | 3 +++ lib/preview-web/src/PreviewWeb.test.ts | 2 ++ lib/store/src/StoryStore.test.ts | 6 ++++++ lib/store/src/types.ts | 2 +- 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/preview-web/src/PreviewWeb.integration.test.ts b/lib/preview-web/src/PreviewWeb.integration.test.ts index 97715ff4a7b0..b1f636beb08d 100644 --- a/lib/preview-web/src/PreviewWeb.integration.test.ts +++ b/lib/preview-web/src/PreviewWeb.integration.test.ts @@ -52,7 +52,7 @@ beforeEach(() => { projectAnnotations.renderToDOM.mockReset(); projectAnnotations.render.mockClear(); projectAnnotations.decorators[0].mockClear(); - projectAnnotations.parameters.docs.renderer = () => new DocsRenderer(); + projectAnnotations.parameters.docs.renderer = () => new DocsRenderer() as any; addons.setChannel(mockChannel as any); addons.setServerChannel(createMockChannel()); diff --git a/lib/preview-web/src/PreviewWeb.mockdata.ts b/lib/preview-web/src/PreviewWeb.mockdata.ts index 23384cb3d0f9..9be71617b30d 100644 --- a/lib/preview-web/src/PreviewWeb.mockdata.ts +++ b/lib/preview-web/src/PreviewWeb.mockdata.ts @@ -55,18 +55,21 @@ export const storyIndex: StoryIndex = { v: 4, entries: { 'component-one--a': { + type: 'story', id: 'component-one--a', title: 'Component One', name: 'A', importPath: './src/ComponentOne.stories.js', }, 'component-one--b': { + type: 'story', id: 'component-one--b', title: 'Component One', name: 'B', importPath: './src/ComponentOne.stories.js', }, 'component-two--c': { + type: 'story', id: 'component-two--c', title: 'Component Two', name: 'C', diff --git a/lib/preview-web/src/PreviewWeb.test.ts b/lib/preview-web/src/PreviewWeb.test.ts index ef52ab5d4eea..5d7c76986d0f 100644 --- a/lib/preview-web/src/PreviewWeb.test.ts +++ b/lib/preview-web/src/PreviewWeb.test.ts @@ -286,6 +286,7 @@ describe('PreviewWeb', () => { entries: { ...storyIndex.entries, 'component-one--d': { + type: 'story', id: 'component-one--d', title: 'Component One', name: 'D', @@ -336,6 +337,7 @@ describe('PreviewWeb', () => { entries: { ...storyIndex.entries, 'component-one--d': { + type: 'story', id: 'component-one--d', title: 'Component One', name: 'D', diff --git a/lib/store/src/StoryStore.test.ts b/lib/store/src/StoryStore.test.ts index fc844987e42d..6c08fbdda6c3 100644 --- a/lib/store/src/StoryStore.test.ts +++ b/lib/store/src/StoryStore.test.ts @@ -46,18 +46,21 @@ const storyIndex: StoryIndex = { v: 4, entries: { 'component-one--a': { + type: 'story', id: 'component-one--a', title: 'Component One', name: 'A', importPath: './src/ComponentOne.stories.js', }, 'component-one--b': { + type: 'story', id: 'component-one--b', title: 'Component One', name: 'B', importPath: './src/ComponentOne.stories.js', }, 'component-two--c': { + type: 'story', id: 'component-two--c', title: 'Component Two', name: 'C', @@ -235,6 +238,7 @@ describe('StoryStore', () => { entries: { ...storyIndex.entries, 'new-component--story': { + type: 'story', id: 'new-component--story', title: 'New Component', name: 'Story', @@ -266,6 +270,7 @@ describe('StoryStore', () => { v: 4, entries: { 'component-one--a': { + type: 'story', id: 'component-one--a', title: 'Component One', name: 'A', @@ -296,6 +301,7 @@ describe('StoryStore', () => { v: 4, entries: { 'component-one--a': { + type: 'story', id: 'component-one--a', title: 'Component One', name: 'A', diff --git a/lib/store/src/types.ts b/lib/store/src/types.ts index 17ca8a9a986e..e8afb7db1757 100644 --- a/lib/store/src/types.ts +++ b/lib/store/src/types.ts @@ -98,7 +98,7 @@ interface BaseIndexEntry { importPath: Path; } export type StoryIndexEntry = BaseIndexEntry & { - type?: 'story'; + type: 'story'; }; export type DocsIndexEntry = BaseIndexEntry & {