diff --git a/lib/blocks/src/blocks/ExternalDocsContainer.tsx b/lib/blocks/src/blocks/ExternalDocsContainer.tsx deleted file mode 100644 index 38660fe56ab4..000000000000 --- a/lib/blocks/src/blocks/ExternalDocsContainer.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; - -import { ThemeProvider, themes, ensure } from '@storybook/theming'; -import { DocsContextProps } from '@storybook/preview-web'; -import { ModuleExport, ModuleExports, Story } from '@storybook/store'; -import { AnyFramework, StoryId, StoryName } from '@storybook/csf'; - -import { DocsContext } from './DocsContext'; -import { ExternalPreview } from './ExternalPreview'; - -let preview: ExternalPreview; - -export const ExternalDocsContainer: React.FC<{ projectAnnotations: any }> = ({ - projectAnnotations, - children, -}) => { - if (!preview) preview = new ExternalPreview(projectAnnotations); - - let pageMeta: ModuleExport; - const setMeta = (m: ModuleExport) => { - pageMeta = m; - }; - - const docsContext: DocsContextProps = { - id: 'external-docs', - title: 'External', - name: 'Docs', - - storyIdByModuleExport: (storyExport: ModuleExport, metaExport: ModuleExports) => { - return preview.storyIdByModuleExport(storyExport, metaExport || pageMeta); - }, - - storyIdByName: (name: StoryName) => { - // TODO - throw new Error('not implemented'); - }, - - storyById: (id: StoryId) => { - return preview.storyById(id); - }, - - getStoryContext: () => { - throw new Error('not implemented'); - }, - - componentStories: () => { - // TODO: could implement in a very similar way to in DocsRender. (TODO: How to share code?) - throw new Error('not implemented'); - }, - - loadStory: async (id: StoryId) => { - return preview.storyById(id); - }, - - renderStoryToElement: (story: Story, element: HTMLElement) => { - return preview.renderStoryToElement(story, element); - }, - - setMeta, - }; - - return ( - - {children} - - ); -}; diff --git a/lib/blocks/src/blocks/ExternalPreview.test.ts b/lib/blocks/src/blocks/ExternalPreview.test.ts deleted file mode 100644 index 3dc820f1bcf9..000000000000 --- a/lib/blocks/src/blocks/ExternalPreview.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { StoryId } from '@storybook/csf'; -import { ExternalPreview } from './ExternalPreview'; - -const projectAnnotations = { render: jest.fn(), renderToDOM: jest.fn() }; -const csfFileWithTitle = { - default: { title: 'Component' }, - - one: { args: { a: 'foo' } }, - two: { args: { b: 'bar' } }, -}; -const csfFileWithoutTitle = { - default: {}, - - one: { args: { a: 'foo' } }, -}; - -describe('ExternalPreview', () => { - describe('storyIdByModuleExport and storyById', () => { - it('handles csf files with titles', async () => { - const preview = new ExternalPreview(projectAnnotations); - - const storyId = preview.storyIdByModuleExport( - csfFileWithTitle.one, - csfFileWithTitle - ) as StoryId; - const story = preview.storyById(storyId); - - expect(story).toMatchObject({ - title: 'Component', - initialArgs: { a: 'foo' }, - }); - }); - - it('returns consistent story ids and objects', () => { - const preview = new ExternalPreview(projectAnnotations); - - const storyId = preview.storyIdByModuleExport( - csfFileWithTitle.one, - csfFileWithTitle - ) as StoryId; - const story = preview.storyById(storyId); - - expect(preview.storyIdByModuleExport(csfFileWithTitle.one, csfFileWithTitle)).toEqual( - storyId - ); - expect(preview.storyById(storyId)).toBe(story); - }); - - it('handles more than one export', async () => { - const preview = new ExternalPreview(projectAnnotations); - - preview.storyById( - preview.storyIdByModuleExport(csfFileWithTitle.one, csfFileWithTitle) as StoryId - ); - - const story = preview.storyById( - preview.storyIdByModuleExport(csfFileWithTitle.two, csfFileWithTitle) as StoryId - ); - expect(story).toMatchObject({ - title: 'Component', - initialArgs: { b: 'bar' }, - }); - }); - - it('handles csf files without titles', async () => { - const preview = new ExternalPreview(projectAnnotations); - - const storyId = preview.storyIdByModuleExport( - csfFileWithoutTitle.one, - csfFileWithoutTitle - ) as StoryId; - const story = preview.storyById(storyId); - - expect(story).toMatchObject({ - title: expect.any(String), - initialArgs: { a: 'foo' }, - }); - }); - }); -}); diff --git a/lib/blocks/src/blocks/ExternalPreview.ts b/lib/blocks/src/blocks/ExternalPreview.ts deleted file mode 100644 index 87c39d165236..000000000000 --- a/lib/blocks/src/blocks/ExternalPreview.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Preview } from '@storybook/preview-web'; -import { Path, ModuleExports, StoryIndex, ModuleExport } from '@storybook/store'; -import { toId, AnyFramework, ComponentTitle, StoryId, ProjectAnnotations } from '@storybook/csf'; - -type StoryExport = ModuleExport; -type MetaExport = ModuleExports; -type ExportName = string; - -class ConstantMap { - entries = new Map(); - - // eslint-disable-next-line no-useless-constructor - constructor(private prefix: string) {} - - get(key: TKey) { - if (!this.entries.has(key)) { - this.entries.set(key, `${this.prefix}${this.entries.size}` as TValue); - } - return this.entries.get(key); - } -} - -export class ExternalPreview extends Preview { - private initialized = false; - - private importPaths = new ConstantMap('./importPath/'); - - private titles = new ConstantMap('title-'); - - public storyIds = new Map(); - - private storyIndex: StoryIndex = { v: 4, entries: {} }; - - private moduleExportsByImportPath: Record = {}; - - constructor(public projectAnnotations: ProjectAnnotations) { - super(); - } - - storyIdByModuleExport(storyExport: StoryExport, meta: MetaExport) { - if (!this.storyIds.has(storyExport)) this.addStoryFromExports(storyExport, meta); - return this.storyIds.get(storyExport); - } - - addStoryFromExports(storyExport: StoryExport, meta: MetaExport) { - const importPath = this.importPaths.get(meta); - this.moduleExportsByImportPath[importPath] = meta; - - const title = meta.default.title || this.titles.get(meta); - - const exportEntry = Object.entries(meta).find( - ([_, moduleExport]) => moduleExport === storyExport - ); - if (!exportEntry) - throw new Error(`Didn't find \`of\` used in Story block in the provided CSF exports`); - const storyId = toId(title, exportEntry[0]); - this.storyIds.set(storyExport, storyId); - - this.storyIndex.entries[storyId] = { - id: storyId, - importPath, - title, - name: 'Name', - type: 'story', - }; - - if (!this.initialized) { - this.initialized = true; - return this.initialize({ - getStoryIndex: () => this.storyIndex, - importFn: (path: Path) => { - return Promise.resolve(this.moduleExportsByImportPath[path]); - }, - getProjectAnnotations: () => this.projectAnnotations, - }); - } - // else - return this.onStoriesChanged({ storyIndex: this.storyIndex }); - } - - storyById(storyId: StoryId) { - const entry = this.storyIndex.entries[storyId]; - if (!entry) throw new Error(`Unknown storyId ${storyId}`); - const { importPath, title } = entry; - const moduleExports = this.moduleExportsByImportPath[importPath]; - const csfFile = this.storyStore.processCSFFileWithCache( - moduleExports, - importPath, - title - ); - return this.storyStore.storyFromCSFFile({ storyId, csfFile }); - } -} diff --git a/lib/blocks/src/blocks/external/ExternalDocsContainer.tsx b/lib/blocks/src/blocks/external/ExternalDocsContainer.tsx new file mode 100644 index 000000000000..6b4a3efa6476 --- /dev/null +++ b/lib/blocks/src/blocks/external/ExternalDocsContainer.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { ThemeProvider, themes, ensure } from '@storybook/theming'; +import { AnyFramework } from '@storybook/csf'; + +import { DocsContext } from '../DocsContext'; +import { ExternalPreview } from './ExternalPreview'; + +let preview: ExternalPreview; + +export const ExternalDocsContainer: React.FC<{ projectAnnotations: any }> = ({ + projectAnnotations, + children, +}) => { + if (!preview) preview = new ExternalPreview(projectAnnotations); + + return ( + + {children} + + ); +}; diff --git a/lib/blocks/src/blocks/external/ExternalDocsContext.ts b/lib/blocks/src/blocks/external/ExternalDocsContext.ts new file mode 100644 index 000000000000..70c69a32375a --- /dev/null +++ b/lib/blocks/src/blocks/external/ExternalDocsContext.ts @@ -0,0 +1,31 @@ +import { StoryId, AnyFramework, ComponentTitle, StoryName } from '@storybook/csf'; +import { DocsContext, DocsContextProps } from '@storybook/preview-web'; +import { CSFFile, ModuleExport, ModuleExports, StoryStore } from '@storybook/store'; + +export class ExternalDocsContext extends DocsContext { + constructor( + public readonly id: StoryId, + public readonly title: ComponentTitle, + public readonly name: StoryName, + protected store: StoryStore, + public renderStoryToElement: DocsContextProps['renderStoryToElement'], + private processMetaExports: (metaExports: ModuleExports) => CSFFile + ) { + super(id, title, name, store, renderStoryToElement, [], true); + } + + setMeta = (metaExports: ModuleExports) => { + const csfFile = this.processMetaExports(metaExports); + this.referenceCSFFile(csfFile, true); + }; + + storyIdByModuleExport(storyExport: ModuleExport, metaExports?: ModuleExports) { + if (metaExports) { + const csfFile = this.processMetaExports(metaExports); + this.referenceCSFFile(csfFile, false); + } + + // This will end up looking up the story id in the CSF file referenced above or via setMeta() + return super.storyIdByModuleExport(storyExport); + } +} diff --git a/lib/blocks/src/blocks/external/ExternalPreview.ts b/lib/blocks/src/blocks/external/ExternalPreview.ts new file mode 100644 index 000000000000..cf3fbd2a6b5b --- /dev/null +++ b/lib/blocks/src/blocks/external/ExternalPreview.ts @@ -0,0 +1,84 @@ +import { Preview } from '@storybook/preview-web'; +import { Path, ModuleExports, StoryIndex, composeConfigs } from '@storybook/store'; +import { toId, AnyFramework, ComponentTitle, ProjectAnnotations } from '@storybook/csf'; +import { ExternalDocsContext } from './ExternalDocsContext'; + +type MetaExports = ModuleExports; + +class ConstantMap { + entries = new Map(); + + // eslint-disable-next-line no-useless-constructor + constructor(private prefix: string) {} + + get(key: TKey) { + if (!this.entries.has(key)) { + this.entries.set(key, `${this.prefix}${this.entries.size}` as TValue); + } + return this.entries.get(key); + } +} + +export class ExternalPreview extends Preview { + private importPaths = new ConstantMap('./importPath/'); + + private titles = new ConstantMap('title-'); + + private storyIndex: StoryIndex = { v: 4, entries: {} }; + + private moduleExportsByImportPath: Record = {}; + + constructor(public projectAnnotations: ProjectAnnotations) { + super(); + + this.initialize({ + getStoryIndex: () => this.storyIndex, + importFn: (path: Path) => { + return Promise.resolve(this.moduleExportsByImportPath[path]); + }, + getProjectAnnotations: () => + composeConfigs([ + { parameters: { docs: { inlineStories: true } } }, + this.projectAnnotations, + ]), + }); + } + + processMetaExports = (metaExports: MetaExports) => { + const importPath = this.importPaths.get(metaExports); + this.moduleExportsByImportPath[importPath] = metaExports; + + const title = metaExports.default.title || this.titles.get(metaExports); + + const csfFile = this.storyStore.processCSFFileWithCache( + metaExports, + importPath, + title + ); + + Object.values(csfFile.stories).forEach(({ id, name }) => { + this.storyIndex.entries[id] = { + id, + importPath, + title, + name, + type: 'story', + }; + }); + + this.onStoriesChanged({ storyIndex: this.storyIndex }); + + return csfFile; + }; + + docsContext = () => { + return new ExternalDocsContext( + 'storybook--docs', + 'Storybook', + 'Docs', + this.storyStore, + this.renderStoryToElement.bind(this), + this.processMetaExports.bind(this) + ); + }; +} diff --git a/lib/blocks/src/blocks/index.ts b/lib/blocks/src/blocks/index.ts index 14453d462802..6f235adeac44 100644 --- a/lib/blocks/src/blocks/index.ts +++ b/lib/blocks/src/blocks/index.ts @@ -9,8 +9,8 @@ export * from './DocsRenderer'; export * from './DocsPage'; export * from './DocsContainer'; export * from './DocsStory'; -export * from './ExternalDocsContainer'; -export * from './ExternalPreview'; +export * from './external/ExternalDocsContainer'; +export * from './external/ExternalPreview'; export * from './Heading'; export * from './Meta'; export * from './Preview'; diff --git a/lib/preview-web/src/docs-context/DocsContext.ts b/lib/preview-web/src/docs-context/DocsContext.ts index 7f69f4e5aeb7..5efbd74ab877 100644 --- a/lib/preview-web/src/docs-context/DocsContext.ts +++ b/lib/preview-web/src/docs-context/DocsContext.ts @@ -5,8 +5,7 @@ import { StoryId, StoryName, } from '@storybook/csf'; -import { CSFFile, ModuleExport, Story, StoryStore } from '@storybook/store'; -import { PreviewWeb } from '../PreviewWeb'; +import { CSFFile, ModuleExport, ModuleExports, Story, StoryStore } from '@storybook/store'; import { DocsContextProps } from './DocsContextProps'; @@ -24,8 +23,8 @@ export class DocsContext implements DocsContext public readonly title: ComponentTitle, public readonly name: StoryName, protected store: StoryStore, + public renderStoryToElement: DocsContextProps['renderStoryToElement'], /** The CSF files known (via the index) to be refererenced by this docs file */ - public renderStoryToElement: PreviewWeb['renderStoryToElement'], csfFiles: CSFFile[], componentStoriesFromAllCsfFiles = true ) { @@ -35,27 +34,35 @@ export class DocsContext implements DocsContext this.componentStoriesValue = []; csfFiles.forEach((csfFile, index) => { - Object.values(csfFile.stories).forEach((annotation) => { - this.storyIdToCSFFile.set(annotation.id, csfFile); - this.exportToStoryId.set(annotation.moduleExport, annotation.id); - this.nameToStoryId.set(annotation.name, annotation.id); + this.referenceCSFFile(csfFile, componentStoriesFromAllCsfFiles || index === 0); + }); + } + + // This docs entry references this CSF file and can syncronously load the stories, as well + // as reference them by module export. If the CSF is part of the "component" stories, they + // can also be referenced by name and are in the componentStories list. + referenceCSFFile(csfFile: CSFFile, addToComponentStories: boolean) { + Object.values(csfFile.stories).forEach((annotation) => { + this.storyIdToCSFFile.set(annotation.id, csfFile); + this.exportToStoryId.set(annotation.moduleExport, annotation.id); - if (componentStoriesFromAllCsfFiles || index === 0) - this.componentStoriesValue.push(this.storyById(annotation.id)); - }); + if (addToComponentStories) { + this.nameToStoryId.set(annotation.name, annotation.id); + this.componentStoriesValue.push(this.storyById(annotation.id)); + } }); } - setMeta() { - // Do nothing + setMeta(metaExports: ModuleExports) { + // Do nothing (this is really only used by external docs) } - storyIdByModuleExport = (storyExport: ModuleExport) => { + storyIdByModuleExport(storyExport: ModuleExport, metaExports?: ModuleExports) { const storyId = this.exportToStoryId.get(storyExport); if (storyId) return storyId; throw new Error(`No story found with that export: ${storyExport}`); - }; + } storyIdByName = (storyName: StoryName) => { const storyId = this.nameToStoryId.get(storyName); diff --git a/lib/preview-web/src/index.ts b/lib/preview-web/src/index.ts index 92992bf87195..c1ae35158ea7 100644 --- a/lib/preview-web/src/index.ts +++ b/lib/preview-web/src/index.ts @@ -6,5 +6,6 @@ export { PreviewWeb } from './PreviewWeb'; export { simulatePageLoad, simulateDOMContentLoaded } from './simulate-pageload'; +export { DocsContext } from './docs-context/DocsContext'; export type { DocsContextProps } from './docs-context/DocsContextProps'; export type { DocsRenderFunction } from './docs-context/DocsRenderFunction';