WIP Got Story Rendering working

This commit is contained in:
Tom Coleman 2022-05-03 21:37:17 +10:00
parent feca848f1f
commit f7690afb17
15 changed files with 185 additions and 149 deletions

View File

@ -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<TFramework>
export function renderDocs<TFramework extends AnyFramework>(
docsContext: DocsContextProps<TFramework>,
docsParameters: Parameters,
element: HTMLElement,
callback: () => void
): void {
renderDocsAsync(docsContext, docsParameters, element).then(callback);
}
async function renderDocsAsync<TFramework extends AnyFramework>(
docsContext: DocsContextProps<TFramework>,
docsParameters: Parameters,
element: HTMLElement
) {
console.log(docsParameters);
// FIXME -- use DocsContainer, make it work for modern
const SimpleContainer = ({ children }: any) => (
<DocsContext.Provider value={docsContext}>{children} </DocsContext.Provider>
);
const Container: ComponentType<{ context: DocsContextProps<TFramework> }> =
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 = (
<Container key={docsContext.title} context={docsContext}>
<Page />
</Container>
);
await new Promise<void>((resolve) => {
ReactDOM.render(docsElement, element, resolve);
});
}
export function unmountDocs(element: HTMLElement) {
ReactDOM.unmountComponentAtNode(element);
}

View File

@ -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;

View File

@ -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<StoryProps> = (props) => {
const story = useStory(storyId, context);
const [showLoader, setShowLoader] = useState(true);
console.log(storyId, story);
useEffect(() => {
let cleanup: () => void;
if (story && storyRef.current) {

View File

@ -16,7 +16,7 @@ export function useStories<TFramework extends AnyFramework = AnyFramework>(
storyIds: StoryId[],
context: DocsContextProps<TFramework>
): (Story<TFramework> | void)[] {
const initialStoriesById = context.componentStories().reduce((acc, story) => {
const initialStoriesById = context.preloadedStories().reduce((acc, story) => {
acc[story.id] = story;
return acc;
}, {} as Record<StoryId, Story<TFramework>>);

View File

@ -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;
},
},
};

View File

@ -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';
<Meta of={meta} />
# Docs with of
hello docs
<Story of={Basic} />

View File

@ -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"
},

View File

@ -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<TFramework extends AnyFramework> implements Render<TFramework> {
public type: RenderType = 'docs';
@ -22,10 +22,12 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
private canvasElement?: HTMLElement;
private context?: DocsContextProps;
private context?: DocsContextProps<TFramework>;
public disableKeyListeners = false;
public teardown: (options: { viewModeChanged?: boolean }) => Promise<void>;
constructor(
private channel: Channel,
private store: StoryStore<TFramework>,
@ -56,22 +58,17 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
return this.preparing;
}
async renderToElement(
canvasElement: HTMLElement,
renderStoryToElement: DocsContextProps['renderStoryToElement']
) {
this.canvasElement = canvasElement;
async docsContext(
renderStoryToElement: DocsContextProps<TFramework>['renderStoryToElement']
): Promise<DocsContextProps<TFramework>> {
const { id, title, name } = this.entry;
const csfFile: CSFFile<TFramework> = 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<TFramework>) =>
@ -79,10 +76,45 @@ export class DocsRender<TFramework extends AnyFramework> implements Render<TFram
...this.store.getStoryContext(renderedStory),
viewMode: 'docs' as ViewMode,
} as StoryContextForLoaders<TFramework>),
// 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<TFramework extends AnyFramework> implements Render<TFram
if (!(this.story || this.exports) || !this.context || !this.canvasElement)
throw new Error('DocsRender not ready to render');
const renderer = await import('./renderDocs');
const { docs } = this.story?.parameters || this.store.projectAnnotations.parameters;
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)
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<TFramework>)(
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<TFramework extends AnyFramework> implements Render<TFram
// docs page when a single story changes.
if (!global.FEATURES?.modernInlineRender) await this.render();
}
async teardown({ viewModeChanged }: { viewModeChanged?: boolean } = {}) {
if (!viewModeChanged || !this.canvasElement) return;
const renderer = await import('./renderDocs');
renderer.unmountDocs(this.canvasElement);
}
}

View File

@ -274,7 +274,6 @@ export class PreviewWeb<TFramework extends AnyFramework> extends Preview<TFramew
} else {
render = new DocsRender<TFramework>(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

View File

@ -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<TFramework extends AnyFramework>(
story: Story<TFramework>,
docsContext: DocsContextProps<TFramework>,
element: HTMLElement,
callback: () => void
) {
return renderLegacyDocsAsync(story, docsContext, element).then(callback);
}
export function renderDocs<TFramework extends AnyFramework>(
exports: ModuleExports,
docsContext: DocsContextProps<TFramework>,
element: HTMLElement,
callback: () => void
) {
return renderDocsAsync(exports, docsContext, element).then(callback);
}
async function renderLegacyDocsAsync<TFramework extends AnyFramework>(
story: Story<TFramework>,
docsContext: DocsContextProps<TFramework>,
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<TFramework> }> =
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 = (
<DocsContainer key={story.componentId} context={docsContext}>
<Page />
</DocsContainer>
);
await new Promise<void>((resolve) => {
ReactDOM.render(docsElement, element, resolve);
});
}
async function renderDocsAsync<TFramework extends AnyFramework>(
exports: ModuleExports,
docsContext: DocsContextProps<TFramework>,
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 = (
<DocsContainer>
<Page />
</DocsContainer>
);
await new Promise<void>((resolve) => {
ReactDOM.render(docsElement, element, resolve);
});
}
export function unmountDocs(element: HTMLElement) {
ReactDOM.unmountComponentAtNode(element);
}

View File

@ -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<TFramework extends AnyFramework = AnyFramework> {
legacy: boolean;
id: StoryId;
title: ComponentTitle;
name: StoryName;
storyIdByModuleExport: (moduleExport: any) => StoryId;
storyById: (id: StoryId) => Story<TFramework>;
getStoryContext: (story: Story<TFramework>) => StoryContextForLoaders<TFramework>;
componentStories: () => Story<TFramework>[];
preloadedStories: () => Story<TFramework>[];
loadStory: (id: StoryId) => Promise<Story<TFramework>>;
renderStoryToElement: PreviewWeb<TFramework>['renderStoryToElement'];
getStoryContext: (story: Story<TFramework>) => StoryContextForLoaders<TFramework>;
/**
* mdxStoryNameToKey is an MDX-compiler-generated mapping of an MDX story's
@ -27,16 +33,11 @@ export interface DocsContextProps<TFramework extends AnyFramework = AnyFramework
*/
mdxStoryNameToKey?: Record<string, string>;
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<TFramework extends AnyFramework> = (
docsContext: DocsContextProps<TFramework>,
docsParameters: Parameters,
element: HTMLElement,
callback: () => void
) => void;

View File

@ -62,6 +62,11 @@ export class StoryStore<TFramework extends AnyFramework> {
resolveInitializationPromise: () => void;
/**
* A map of module export to story id, for later consumption
*/
moduleExportMap: Map<any, StoryId> = new Map();
constructor() {
this.globals = new GlobalsStore();
this.args = new ArgsStore();
@ -144,7 +149,7 @@ export class StoryStore<TFramework extends AnyFramework> {
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<TFramework extends AnyFramework> {
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({

View File

@ -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<TFramework extends AnyFramework>(
moduleExports: ModuleExports,
importPath: Path,
title: ComponentTitle
title: ComponentTitle,
moduleExportMap?: Map<any, StoryId>
): CSFFile<TFramework> {
const { default: defaultExport, __namedExportsOrder, ...namedExports } = moduleExports;
@ -51,6 +58,8 @@ export function processCSFFile<TFramework extends AnyFramework>(
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;
}
});

View File

@ -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