diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts index ed753f8b21b6..0065f4492e1f 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.test.ts @@ -246,7 +246,6 @@ describe('StoryIndexGenerator', () => { "id": "page--docs", "importPath": "./src/nested/Page.stories.mdx", "name": "docs", - "standalone": false, "storiesImports": Array [], "tags": Array [ "stories-mdx", @@ -336,7 +335,6 @@ describe('StoryIndexGenerator', () => { "id": "b--docs", "importPath": "./src/B.stories.ts", "name": "docs", - "standalone": false, "storiesImports": Array [], "tags": Array [ "autodocs", @@ -360,7 +358,6 @@ describe('StoryIndexGenerator', () => { "id": "d--docs", "importPath": "./src/D.stories.jsx", "name": "docs", - "standalone": false, "storiesImports": Array [], "tags": Array [ "autodocs", @@ -451,6 +448,21 @@ describe('StoryIndexGenerator', () => { `); }); + it('adds the autodocs tag to the autogenerated docs entries', async () => { + const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( + './src/**/*.stories.(ts|js|jsx)', + options + ); + + const generator = new StoryIndexGenerator([specifier], autodocsTrueOptions); + await generator.initialize(); + + const index = await generator.getIndex(); + expect(index.entries['first-nested-deeply-f--docs'].tags).toEqual( + expect.arrayContaining(['autodocs']) + ); + }); + it('throws an error if you attach a MetaOf entry to a tagged autodocs entry', async () => { const csfSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( './src/B.stories.ts', @@ -494,7 +506,6 @@ describe('StoryIndexGenerator', () => { "id": "a--docs", "importPath": "./src/docs2/MetaOf.mdx", "name": "docs", - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -537,7 +548,6 @@ describe('StoryIndexGenerator', () => { "id": "duplicate-a--docs", "importPath": "./duplicate/A.stories.js", "name": "docs", - "standalone": false, "storiesImports": Array [ "./duplicate/SecondA.stories.js", ], @@ -607,7 +617,6 @@ describe('StoryIndexGenerator', () => { "id": "a--docs", "importPath": "./src/docs2/MetaOf.mdx", "name": "docs", - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -621,7 +630,6 @@ describe('StoryIndexGenerator', () => { "id": "a--second-docs", "importPath": "./src/docs2/SecondMetaOf.mdx", "name": "Second Docs", - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -646,7 +654,6 @@ describe('StoryIndexGenerator', () => { "id": "docs2-yabbadabbadooo--docs", "importPath": "./src/docs2/Title.mdx", "name": "docs", - "standalone": true, "storiesImports": Array [], "tags": Array [ "docs", @@ -658,7 +665,6 @@ describe('StoryIndexGenerator', () => { "id": "notitle--docs", "importPath": "./src/docs2/NoTitle.mdx", "name": "docs", - "standalone": true, "storiesImports": Array [], "tags": Array [ "docs", @@ -746,7 +752,6 @@ describe('StoryIndexGenerator', () => { "id": "a--info", "importPath": "./src/docs2/MetaOf.mdx", "name": "Info", - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -760,7 +765,6 @@ describe('StoryIndexGenerator', () => { "id": "a--second-docs", "importPath": "./src/docs2/SecondMetaOf.mdx", "name": "Second Docs", - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -785,7 +789,6 @@ describe('StoryIndexGenerator', () => { "id": "docs2-yabbadabbadooo--info", "importPath": "./src/docs2/Title.mdx", "name": "Info", - "standalone": true, "storiesImports": Array [], "tags": Array [ "docs", @@ -797,7 +800,6 @@ describe('StoryIndexGenerator', () => { "id": "notitle--info", "importPath": "./src/docs2/NoTitle.mdx", "name": "Info", - "standalone": true, "storiesImports": Array [], "tags": Array [ "docs", @@ -825,7 +827,7 @@ describe('StoryIndexGenerator', () => { }); describe('duplicates', () => { - it('warns when two standalone entries reference the same CSF file without a name', async () => { + it('warns when two MDX entries reference the same CSF file without a name', async () => { const docsErrorSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( './errors/DuplicateMetaOf.mdx', options @@ -853,7 +855,7 @@ describe('StoryIndexGenerator', () => { ); }); - it('warns when a standalone entry has the same name as a story', async () => { + it('warns when a MDX entry has the same name as a story', async () => { const docsErrorSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( './errors/MetaOfClashingName.mdx', options diff --git a/code/lib/core-server/src/utils/StoryIndexGenerator.ts b/code/lib/core-server/src/utils/StoryIndexGenerator.ts index 88ebd6223749..9beeae4e9809 100644 --- a/code/lib/core-server/src/utils/StoryIndexGenerator.ts +++ b/code/lib/core-server/src/utils/StoryIndexGenerator.ts @@ -5,9 +5,8 @@ import slash from 'slash'; import type { IndexEntry, - StandaloneDocsIndexEntry, StoryIndexEntry, - TemplateDocsIndexEntry, + DocsIndexEntry, ComponentTitle, NormalizedStoriesSpecifier, StoryIndexer, @@ -26,17 +25,25 @@ import { getStorySortParameter, NoMetaError } from '@storybook/csf-tools'; import { toId } from '@storybook/csf'; import { analyze } from '@storybook/docs-mdx'; -/** A .mdx file will produce a "standalone" docs entry */ -type DocsCacheEntry = StandaloneDocsIndexEntry; +/** A .mdx file will produce a docs entry */ +type DocsCacheEntry = DocsIndexEntry; /** A *.stories.* file will produce a list of stories and possibly a docs entry */ type StoriesCacheEntry = { - entries: (StoryIndexEntry | TemplateDocsIndexEntry)[]; + entries: (StoryIndexEntry | DocsIndexEntry)[]; dependents: Path[]; type: 'stories'; }; type CacheEntry = false | StoriesCacheEntry | DocsCacheEntry; type SpecifierStoriesCache = Record; +export const AUTODOCS_TAG = 'autodocs'; +export const STORIES_MDX_TAG = 'stories-mdx'; + +/** Was this docs entry generated by a .mdx file? (see discussion below) */ +export function isMdxEntry({ tags }: DocsIndexEntry) { + return !tags?.includes(AUTODOCS_TAG) && !tags?.includes(STORIES_MDX_TAG); +} + export class DuplicateEntriesError extends Error { entries: IndexEntry[]; @@ -67,13 +74,17 @@ const makeAbsolute = (otherImport: Path, normalizedPath: Path, workingDir: Path) * * A stories file is indexed by an indexer (passed in), which produces a list of stories. * - If the stories have the `parameters.docsOnly` setting, they are disregarded. - * - If the indexer is a "docs template" indexer, OR autodocs is enabled, - * a templated docs entry is added pointing to the story file. + * - If the stories have the 'stories-mdx' tag (i.e. were generated by a .stories.mdx file), + * OR autodocs is enabled, a docs entry is added pointing to the story file. + * + * A (modern) docs (.mdx) file is indexed, a docs entry is added. * - * A (modern) docs file is indexed, a standalone docs entry is added. + * In the preview, a docs entry with either the `autodocs` or `stories-mdx` tags will be rendered + * as a CSF file that exports an MDX template on the `docs.page` parameter, whereas + * other docs entries are rendered as MDX files directly. * * The entries are "uniq"-ed and sorted. Stories entries are preferred to docs entries and - * standalone docs entries are preferred to templates (with warnings). + * MDX docs entries are preferred to CSF templates (with warnings). */ export class StoryIndexGenerator { // An internal cache mapping specifiers to a set of path=> @@ -236,12 +247,12 @@ export class StoryIndexGenerator { if (!this.options.docs.disable && csf.stories.length) { const { autodocs } = this.options.docs; - const autodocsOptedIn = - autodocs === true || (autodocs === 'tag' && componentTags.includes('autodocs')); + const componentAutodocs = componentTags.includes(AUTODOCS_TAG); + const autodocsOptedIn = autodocs === true || (autodocs === 'tag' && componentAutodocs); // We need a docs entry attached to the CSF file if either: // a) it is a stories.mdx transpiled to CSF, OR // b) we have docs page enabled for this file - if (componentTags.includes('stories-mdx') || autodocsOptedIn) { + if (componentTags.includes(STORIES_MDX_TAG) || autodocsOptedIn) { const name = this.options.docs.defaultName; const id = toId(csf.meta.title, name); entries.unshift({ @@ -250,9 +261,12 @@ export class StoryIndexGenerator { name, importPath, type: 'docs', - tags: [...componentTags, 'docs'], + tags: [ + ...componentTags, + 'docs', + ...(autodocsOptedIn && !componentAutodocs ? [AUTODOCS_TAG] : []), + ], storiesImports: [], - standalone: false, }); } } @@ -340,7 +354,6 @@ export class StoryIndexGenerator { storiesImports: dependencies.map((dep) => dep.entries[0].importPath), type: 'docs', tags: [...(result.tags || []), 'docs'], - standalone: true, }; return docsEntry; } catch (err) { @@ -353,7 +366,7 @@ export class StoryIndexGenerator { let firstIsBetter = true; if (secondEntry.type === 'story') { firstIsBetter = false; - } else if (secondEntry.standalone && firstEntry.type === 'docs' && !firstEntry.standalone) { + } else if (isMdxEntry(secondEntry) && firstEntry.type === 'docs' && !isMdxEntry(firstEntry)) { firstIsBetter = false; } const betterEntry = firstIsBetter ? firstEntry : secondEntry; @@ -369,7 +382,7 @@ export class StoryIndexGenerator { ]); if (betterEntry.type === 'story') { - const worseDescriptor = worseEntry.standalone + const worseDescriptor = isMdxEntry(worseEntry) ? `component docs page` : `automatically generated docs page`; if (betterEntry.name === this.options.docs.defaultName) { @@ -381,18 +394,18 @@ export class StoryIndexGenerator { `🚨 You have a story for ${betterEntry.title} with the same name as your ${worseDescriptor} (${worseEntry.name}), so the docs page is being dropped. ${changeDocsName}` ); } - } else if (betterEntry.standalone) { - // Both entries are standalone but pointing at the same place - if (worseEntry.standalone) { + } else if (isMdxEntry(betterEntry)) { + // Both entries are MDX but pointing at the same place + if (isMdxEntry(worseEntry)) { logger.warn( `🚨 You have two component docs pages with the same name ${betterEntry.title}:${betterEntry.name}. ${changeDocsName}` ); } // If you link a file to a tagged CSF file, you have probably made a mistake - if (worseEntry.tags?.includes('autodocs')) + if (worseEntry.tags?.includes(AUTODOCS_TAG) && this.options.docs.autodocs !== true) throw new Error( - `You created a component docs page for ${worseEntry.title} (${betterEntry.importPath}), but also tagged the CSF file (${worseEntry.importPath}) with 'autodocs'. This is probably a mistake.` + `You created a component docs page for ${worseEntry.title} (${betterEntry.importPath}), but also tagged the CSF file (${worseEntry.importPath}) with '${AUTODOCS_TAG}'. This is probably a mistake.` ); // Otherwise the existing entry is created by `autodocs=true` which allowed to be overridden. diff --git a/code/lib/core-server/src/utils/stories-json.test.ts b/code/lib/core-server/src/utils/stories-json.test.ts index c4a90dea99e9..9dfa026e4c17 100644 --- a/code/lib/core-server/src/utils/stories-json.test.ts +++ b/code/lib/core-server/src/utils/stories-json.test.ts @@ -121,7 +121,6 @@ describe('useStoriesJson', () => { "id": "a--docs", "importPath": "./src/docs2/MetaOf.mdx", "name": "docs", - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -135,7 +134,6 @@ describe('useStoriesJson', () => { "id": "a--second-docs", "importPath": "./src/docs2/SecondMetaOf.mdx", "name": "Second Docs", - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -182,7 +180,6 @@ describe('useStoriesJson', () => { "id": "docs2-notitle--docs", "importPath": "./src/docs2/NoTitle.mdx", "name": "docs", - "standalone": true, "storiesImports": Array [], "tags": Array [ "docs", @@ -194,7 +191,6 @@ describe('useStoriesJson', () => { "id": "docs2-yabbadabbadooo--docs", "importPath": "./src/docs2/Title.mdx", "name": "docs", - "standalone": true, "storiesImports": Array [], "tags": Array [ "docs", @@ -227,7 +223,6 @@ describe('useStoriesJson', () => { "id": "nested-page--docs", "importPath": "./src/nested/Page.stories.mdx", "name": "docs", - "standalone": false, "storiesImports": Array [], "tags": Array [ "stories-mdx", @@ -292,7 +287,6 @@ describe('useStoriesJson', () => { "docsOnly": true, "fileName": "./src/docs2/MetaOf.mdx", }, - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -312,7 +306,6 @@ describe('useStoriesJson', () => { "docsOnly": true, "fileName": "./src/docs2/SecondMetaOf.mdx", }, - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], @@ -383,7 +376,6 @@ describe('useStoriesJson', () => { "docsOnly": true, "fileName": "./src/docs2/NoTitle.mdx", }, - "standalone": true, "storiesImports": Array [], "story": "docs", "tags": Array [ @@ -401,7 +393,6 @@ describe('useStoriesJson', () => { "docsOnly": true, "fileName": "./src/docs2/Title.mdx", }, - "standalone": true, "storiesImports": Array [], "story": "docs", "tags": Array [ @@ -452,7 +443,6 @@ describe('useStoriesJson', () => { "docsOnly": true, "fileName": "./src/nested/Page.stories.mdx", }, - "standalone": false, "storiesImports": Array [], "story": "docs", "tags": Array [ @@ -944,7 +934,6 @@ describe('convertToIndexV3', () => { storiesImports: ['./src/A.stories.js'], title: 'A', type: 'docs', - standalone: true, }, 'a--story-one': { id: 'a--story-one', @@ -976,7 +965,6 @@ describe('convertToIndexV3', () => { "docsOnly": true, "fileName": "./src/docs2/MetaOf.mdx", }, - "standalone": true, "storiesImports": Array [ "./src/A.stories.js", ], diff --git a/code/lib/core-server/src/utils/summarizeIndex.ts b/code/lib/core-server/src/utils/summarizeIndex.ts index fca99cb25e97..afe09812fe7a 100644 --- a/code/lib/core-server/src/utils/summarizeIndex.ts +++ b/code/lib/core-server/src/utils/summarizeIndex.ts @@ -1,5 +1,7 @@ import type { StoryIndex } from '@storybook/types'; +import { STORIES_MDX_TAG, isMdxEntry, AUTODOCS_TAG } from './StoryIndexGenerator'; + export function summarizeIndex(storyIndex: StoryIndex) { let storyCount = 0; let autodocsCount = 0; @@ -9,11 +11,11 @@ export function summarizeIndex(storyIndex: StoryIndex) { if (entry.type === 'story') { storyCount += 1; } else if (entry.type === 'docs') { - if (entry.standalone) { + if (isMdxEntry(entry)) { mdxCount += 1; - } else if (entry.importPath.endsWith('.mdx')) { + } else if (entry.tags.includes(STORIES_MDX_TAG)) { storiesMdxCount += 1; - } else { + } else if (entry.tags.includes(AUTODOCS_TAG)) { autodocsCount += 1; } } diff --git a/code/lib/manager-api/src/lib/stories.ts b/code/lib/manager-api/src/lib/stories.ts index d2e24bf0c5e0..ba84d135bcfd 100644 --- a/code/lib/manager-api/src/lib/stories.ts +++ b/code/lib/manager-api/src/lib/stories.ts @@ -69,7 +69,7 @@ const transformSetStoriesStoryDataToPreparedStoryIndex = ( if (docsOnly) { acc[id] = { type: 'docs', - standalone: false, + tags: ['stories-mdx'], storiesImports: [], ...base, }; @@ -106,7 +106,7 @@ const transformStoryIndexV3toV4 = (index: StoryIndexV3): API_PreparedStoryIndex } acc[entry.id] = { type, - ...(type === 'docs' && { standalone: false, storiesImports: [] }), + ...(type === 'docs' && { tags: ['stories-mdx'], storiesImports: [] }), ...entry, }; return acc; diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index f7926cd1e4b2..25bd3355c245 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -457,7 +457,7 @@ describe('stories API', () => { name: 'Docs', importPath: './path/to/component-b.ts', storiesImports: [], - standalone: false, + tags: ['stories-mdx'], }, 'component-c--story-4': { type: 'story', @@ -1077,7 +1077,6 @@ describe('stories API', () => { ...navigationEntries, 'intro--docs': { type: 'docs', - standalone: true, id: 'intro--docs', title: 'Intro', name: 'Page', diff --git a/code/lib/preview-api/src/modules/client-api/StoryStoreFacade.ts b/code/lib/preview-api/src/modules/client-api/StoryStoreFacade.ts index 738a135a27b2..ddd0867aef66 100644 --- a/code/lib/preview-api/src/modules/client-api/StoryStoreFacade.ts +++ b/code/lib/preview-api/src/modules/client-api/StoryStoreFacade.ts @@ -21,6 +21,9 @@ import { logger } from '@storybook/client-logger'; import type { StoryStore } from '../../store'; import { userOrAutoTitle, sortStoriesV6 } from '../../store'; +export const AUTODOCS_TAG = 'autodocs'; +export const STORIES_MDX_TAG = 'stories-mdx'; + export class StoryStoreFacade { projectAnnotations: NormalizedProjectAnnotations; @@ -195,22 +198,25 @@ export class StoryStoreFacade { // NOTE: this logic is equivalent to the `extractStories` function of `StoryIndexGenerator` const docsOptions = (global.DOCS_OPTIONS || {}) as DocsOptions; - const autodocsOptedIn = - docsOptions.autodocs === true || - (docsOptions.autodocs === 'tag' && componentTags.includes('autodocs')); + const { autodocs } = docsOptions; + const componentAutodocs = componentTags.includes(AUTODOCS_TAG); + const autodocsOptedIn = autodocs === true || (autodocs === 'tag' && componentAutodocs); if (!docsOptions.disable && storyExports.length) { - if (componentTags.includes('stories-mdx') || autodocsOptedIn) { + if (componentTags.includes(STORIES_MDX_TAG) || autodocsOptedIn) { const name = docsOptions.defaultName; const docsId = toId(componentId || title, name); this.entries[docsId] = { type: 'docs', - standalone: false, id: docsId, title, name, importPath: fileName, ...(componentId && { componentId }), - tags: [...componentTags, 'docs'], + tags: [ + ...componentTags, + 'docs', + ...(autodocsOptedIn && !componentAutodocs ? [AUTODOCS_TAG] : []), + ], storiesImports: [], }; } diff --git a/code/lib/preview-api/src/modules/core-client/PreviewWeb.mockdata.ts b/code/lib/preview-api/src/modules/core-client/PreviewWeb.mockdata.ts deleted file mode 100644 index ae38d545a449..000000000000 --- a/code/lib/preview-api/src/modules/core-client/PreviewWeb.mockdata.ts +++ /dev/null @@ -1,200 +0,0 @@ -/// ; - -import { EventEmitter } from 'events'; -import { - DOCS_RENDERED, - STORY_ERRORED, - STORY_MISSING, - STORY_RENDERED, - STORY_RENDER_PHASE_CHANGED, - STORY_THREW_EXCEPTION, -} from '@storybook/core-events'; - -import type { StoryIndex, TeardownRenderToCanvas } from '@storybook/types'; - -export type RenderPhase = - | 'preparing' - | 'loading' - | 'rendering' - | 'playing' - | 'played' - | 'completed' - | 'aborted' - | 'errored'; - -export const componentOneExports = { - default: { - title: 'Component One', - argTypes: { - foo: { type: { name: 'string' } }, - }, - loaders: [jest.fn()], - parameters: { - docs: { page: jest.fn(), container: jest.fn() }, - }, - }, - a: { args: { foo: 'a' }, play: jest.fn() }, - b: { args: { foo: 'b' }, play: jest.fn() }, -}; -export const componentTwoExports = { - default: { title: 'Component Two' }, - c: { args: { foo: 'c' } }, -}; -export const standaloneDocsExports = { - default: jest.fn(), -}; -// If a second file defines stories for componentOne -export const extraComponentOneExports = { - default: { - title: 'Component One', - parameters: { - docs: { page: jest.fn() }, - }, - }, - e: {}, -}; -export const importFn = jest.fn( - async (path: string) => - ({ - './src/ComponentOne.stories.js': componentOneExports, - './src/ComponentTwo.stories.js': componentTwoExports, - './src/Introduction.mdx': standaloneDocsExports, - './src/ExtraComponentOne.stories.js': extraComponentOneExports, - }[path]) -); - -export const docsRenderer = { - render: jest.fn().mockImplementation((context, parameters, element, cb) => cb()), - unmount: jest.fn(), -}; -export const teardownrenderToCanvas: jest.Mock = jest.fn(); -export const projectAnnotations = { - globals: { a: 'b' }, - globalTypes: {}, - decorators: [jest.fn((s) => s())], - render: jest.fn(), - renderToCanvas: jest.fn().mockReturnValue(teardownrenderToCanvas), - parameters: { docs: { renderer: () => docsRenderer } }, -}; -export const getProjectAnnotations = jest.fn(() => projectAnnotations as any); - -export const storyIndex: StoryIndex = { - v: 4, - entries: { - 'component-one--docs': { - type: 'docs', - id: 'component-one--docs', - title: 'Component One', - name: 'Docs', - importPath: './src/ComponentOne.stories.js', - storiesImports: ['./src/ExtraComponentOne.stories.js'], - standalone: false, - }, - '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-one--e': { - type: 'story', - id: 'component-one--e', - title: 'Component One', - name: 'E', - importPath: './src/ExtraComponentOne.stories.js', - }, - 'component-two--docs': { - type: 'docs', - id: 'component-two--docs', - title: 'Component Two', - name: 'Docs', - importPath: './src/ComponentTwo.stories.js', - storiesImports: [], - standalone: false, - }, - 'component-two--c': { - type: 'story', - id: 'component-two--c', - title: 'Component Two', - name: 'C', - importPath: './src/ComponentTwo.stories.js', - }, - 'introduction--docs': { - type: 'docs', - id: 'introduction--docs', - title: 'Introduction', - name: 'Docs', - importPath: './src/Introduction.mdx', - storiesImports: ['./src/ComponentTwo.stories.js'], - standalone: true, - }, - }, -}; -export const getStoryIndex = () => storyIndex; - -export const emitter = new EventEmitter(); -export const mockChannel = { - on: emitter.on.bind(emitter), - off: emitter.off.bind(emitter), - removeListener: emitter.off.bind(emitter), - emit: jest.fn(emitter.emit.bind(emitter)), - // emit: emitter.emit.bind(emitter), -}; - -export const waitForEvents = ( - events: string[], - predicate: (...args: any[]) => boolean = () => true, - debugLabel?: string -) => { - // We've already emitted a render event. NOTE if you want to test a second call, - // ensure you call `mockChannel.emit.mockClear()` before `waitFor...` - if ( - mockChannel.emit.mock.calls.find( - (call: string[]) => events.includes(call[0]) && predicate(...call.slice(1)) - ) - ) { - return Promise.resolve(null); - } - - return new Promise((resolve, reject) => { - const listener = (...args: any[]) => { - if (!predicate(...args)) return; - events.forEach((event) => mockChannel.off(event, listener)); - resolve(null); - }; - events.forEach((event) => mockChannel.on(event, listener)); - - // Don't wait too long - waitForQuiescence().then(() => { - reject(new Error(`Event was not emitted in time: ${debugLabel || events}`)); - }); - }); -}; - -// The functions on the preview that trigger rendering don't wait for -// the async parts, so we need to listen for the "done" events -export const waitForRender = () => - waitForEvents([ - STORY_RENDERED, - DOCS_RENDERED, - STORY_THREW_EXCEPTION, - STORY_ERRORED, - STORY_MISSING, - ]); - -export const waitForRenderPhase = (phase: RenderPhase) => { - const label = `${STORY_RENDER_PHASE_CHANGED} to ${phase}`; - return waitForEvents([STORY_RENDER_PHASE_CHANGED], ({ newPhase }) => newPhase === phase, label); -}; - -// A little trick to ensure that we always call the real `setTimeout` even when timers are mocked -const realSetTimeout = setTimeout; -export const waitForQuiescence = async () => new Promise((r) => realSetTimeout(r, 100)); diff --git a/code/lib/preview-api/src/modules/core-client/start.test.ts b/code/lib/preview-api/src/modules/core-client/start.test.ts index 3cd268f085dc..bf393aacdf83 100644 --- a/code/lib/preview-api/src/modules/core-client/start.test.ts +++ b/code/lib/preview-api/src/modules/core-client/start.test.ts @@ -15,7 +15,7 @@ import { waitForQuiescence, emitter, mockChannel, -} from './PreviewWeb.mockdata'; +} from '../preview-web/PreviewWeb.mockdata'; import { start as realStart } from './start'; import type { Loadable } from './executeLoadable'; @@ -990,7 +990,6 @@ describe('start', () => { "id": "introduction", "importPath": "./Introduction.stories.mdx", "name": undefined, - "standalone": false, "storiesImports": Array [], "tags": Array [ "stories-mdx", @@ -1221,7 +1220,6 @@ describe('start', () => { "id": "component-b--docs", "importPath": "file2", "name": "Docs", - "standalone": false, "storiesImports": Array [], "tags": Array [ "autodocs", @@ -1255,7 +1253,6 @@ describe('start', () => { "id": "component-c--docs", "importPath": "exports-map-0", "name": "Docs", - "standalone": false, "storiesImports": Array [], "tags": Array [ "component-tag", diff --git a/code/lib/preview-api/src/modules/preview-web/Preview.tsx b/code/lib/preview-api/src/modules/preview-web/Preview.tsx index 4864dfd8c200..f1c2da44c15b 100644 --- a/code/lib/preview-api/src/modules/preview-web/Preview.tsx +++ b/code/lib/preview-api/src/modules/preview-web/Preview.tsx @@ -32,8 +32,8 @@ import { addons } from '../addons'; import { StoryStore } from '../../store'; import { StoryRender } from './render/StoryRender'; -import type { TemplateDocsRender } from './render/TemplateDocsRender'; -import type { StandaloneDocsRender } from './render/StandaloneDocsRender'; +import type { CsfDocsRender } from './render/CsfDocsRender'; +import type { MdxDocsRender } from './render/MdxDocsRender'; const { fetch } = global; @@ -339,10 +339,7 @@ export class Preview { } async teardownRender( - render: - | StoryRender - | TemplateDocsRender - | StandaloneDocsRender, + render: StoryRender | CsfDocsRender | MdxDocsRender, { viewModeChanged }: { viewModeChanged?: boolean } = {} ) { this.storyRenders = this.storyRenders.filter((r) => r !== render); diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts index ad9e70fc02e2..74af0dfbf99b 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts @@ -31,7 +31,7 @@ export const componentTwoExports = { default: { title: 'Component Two' }, c: { args: { foo: 'c' } }, }; -export const standaloneDocsExports = { +export const unattachedDocsExports = { default: jest.fn(), }; // If a second file defines stories for componentOne @@ -49,7 +49,7 @@ export const importFn: jest.Mocked = jest.fn( ({ './src/ComponentOne.stories.js': componentOneExports, './src/ComponentTwo.stories.js': componentTwoExports, - './src/Introduction.mdx': standaloneDocsExports, + './src/Introduction.mdx': unattachedDocsExports, './src/ExtraComponentOne.stories.js': extraComponentOneExports, }[path] || {}) ); @@ -79,7 +79,7 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/ComponentOne.stories.js', storiesImports: ['./src/ExtraComponentOne.stories.js'], - standalone: false, + tags: ['autodocs'], }, 'component-one--a': { type: 'story', @@ -109,7 +109,7 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/ComponentTwo.stories.js', storiesImports: [], - standalone: false, + tags: ['autodocs'], }, 'component-two--c': { type: 'story', @@ -125,7 +125,6 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/Introduction.mdx', storiesImports: ['./src/ComponentTwo.stories.js'], - standalone: true, }, }, }; diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts index 5fee6c1d0453..bfc4426be6d2 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts @@ -48,7 +48,7 @@ import { waitForQuiescence, waitForRenderPhase, docsRenderer, - standaloneDocsExports, + unattachedDocsExports, teardownrenderToCanvas, } from './PreviewWeb.mockdata'; import { WebView } from './WebView'; @@ -701,7 +701,7 @@ describe('PreviewWeb', () => { }); }); - describe('standalone docs entries', () => { + describe('mdx docs entries', () => { it('always renders in docs viewMode', async () => { document.location.search = '?id=introduction--docs'; await createAndRenderPreview(); @@ -723,7 +723,7 @@ describe('PreviewWeb', () => { expect(docsRenderer.render).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ - page: standaloneDocsExports.default, + page: unattachedDocsExports.default, renderer: projectAnnotations.parameters.docs.renderer, }), 'docs-element', @@ -1702,7 +1702,7 @@ describe('PreviewWeb', () => { expect(preview.view.showErrorDisplay).not.toHaveBeenCalled(); }); - it('does NOT render a second time in standalone docs mode', async () => { + it('does NOT render a second time in mdx docs mode', async () => { document.location.search = '?id=introduction--docs&viewMode=docs'; const [gate, openGate] = createGate(); @@ -3098,11 +3098,11 @@ describe('PreviewWeb', () => { }); }); - describe('when a standalone docs file changes', () => { - const newStandaloneDocsExports = { default: jest.fn() }; + describe('when a mdx docs file changes', () => { + const newUnattachedDocsExports = { default: jest.fn() }; const newImportFn = jest.fn(async (path: string) => { - return path === './src/Introduction.mdx' ? newStandaloneDocsExports : importFn(path); + return path === './src/Introduction.mdx' ? newUnattachedDocsExports : importFn(path); }); it('renders with the generated docs parameters', async () => { @@ -3117,7 +3117,7 @@ describe('PreviewWeb', () => { expect(docsRenderer.render).toHaveBeenCalledWith( expect.any(Object), expect.objectContaining({ - page: newStandaloneDocsExports.default, + page: newUnattachedDocsExports.default, renderer: projectAnnotations.parameters.docs.renderer, }), 'docs-element', diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx index 20fe1c96397b..9492003f38f3 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx @@ -27,6 +27,7 @@ import type { ProjectAnnotations, StoryId, ViewMode, + DocsIndexEntry, } from '@storybook/types'; import type { MaybePromise } from './Preview'; @@ -34,8 +35,8 @@ import { Preview } from './Preview'; import { PREPARE_ABORTED } from './render/Render'; import { StoryRender } from './render/StoryRender'; -import { TemplateDocsRender } from './render/TemplateDocsRender'; -import { StandaloneDocsRender } from './render/StandaloneDocsRender'; +import { CsfDocsRender } from './render/CsfDocsRender'; +import { MdxDocsRender } from './render/MdxDocsRender'; import type { Selection, SelectionStore } from './SelectionStore'; import type { View } from './View'; import type { StorySpecifier } from '../store/StoryIndexStore'; @@ -47,10 +48,18 @@ function focusInInput(event: Event) { return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null; } +export const AUTODOCS_TAG = 'autodocs'; +export const STORIES_MDX_TAG = 'stories-mdx'; + +/** Was this docs entry generated by a .mdx file? (see discussion below) */ +export function isMdxEntry({ tags }: DocsIndexEntry) { + return !tags?.includes(AUTODOCS_TAG) && !tags?.includes(STORIES_MDX_TAG); +} + type PossibleRender = | StoryRender - | TemplateDocsRender - | StandaloneDocsRender; + | CsfDocsRender + | MdxDocsRender; function isStoryRender( r: PossibleRender @@ -217,8 +226,8 @@ export class PreviewWithSelection extends Preview extends Preview(this.channel, this.storyStore, entry); + } else if (isMdxEntry(entry)) { + render = new MdxDocsRender(this.channel, this.storyStore, entry); } else { - render = new TemplateDocsRender(this.channel, this.storyStore, entry); + render = new CsfDocsRender(this.channel, this.storyStore, entry); } // We need to store this right away, so if the story changes during diff --git a/code/lib/preview-api/src/modules/preview-web/render/TemplateDocsRender.test.ts b/code/lib/preview-api/src/modules/preview-web/render/CsfDocsRender.test.ts similarity index 79% rename from code/lib/preview-api/src/modules/preview-web/render/TemplateDocsRender.test.ts rename to code/lib/preview-api/src/modules/preview-web/render/CsfDocsRender.test.ts index 895f954abfa7..7c25ec6ee4df 100644 --- a/code/lib/preview-api/src/modules/preview-web/render/TemplateDocsRender.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/render/CsfDocsRender.test.ts @@ -1,9 +1,9 @@ import { Channel } from '@storybook/channels'; -import type { Renderer, TemplateDocsIndexEntry } from '@storybook/types'; +import type { Renderer, DocsIndexEntry } from '@storybook/types'; import type { StoryStore } from '../../store'; import { PREPARE_ABORTED } from './Render'; -import { TemplateDocsRender } from './TemplateDocsRender'; +import { CsfDocsRender } from './CsfDocsRender'; const entry = { type: 'docs', @@ -12,8 +12,8 @@ const entry = { title: 'Component', importPath: './Component.stories.ts', storiesImports: [], - standalone: false, -} as TemplateDocsIndexEntry; + tags: ['autodocs'], +} as DocsIndexEntry; const createGate = (): [Promise, (_?: any) => void] => { let openGate = (_?: any) => {}; @@ -23,7 +23,7 @@ const createGate = (): [Promise, (_?: any) => void] => { return [gate, openGate]; }; -describe('TemplateDocsRender', () => { +describe('CsfDocsRender', () => { it('throws PREPARE_ABORTED if torndown during prepare', async () => { const [importGate, openImportGate] = createGate(); const mockStore = { @@ -33,7 +33,7 @@ describe('TemplateDocsRender', () => { }), }; - const render = new TemplateDocsRender( + const render = new CsfDocsRender( new Channel(), mockStore as unknown as StoryStore, entry diff --git a/code/lib/preview-api/src/modules/preview-web/render/TemplateDocsRender.ts b/code/lib/preview-api/src/modules/preview-web/render/CsfDocsRender.ts similarity index 92% rename from code/lib/preview-api/src/modules/preview-web/render/TemplateDocsRender.ts rename to code/lib/preview-api/src/modules/preview-web/render/CsfDocsRender.ts index f25a7b0ea698..164b62a589be 100644 --- a/code/lib/preview-api/src/modules/preview-web/render/TemplateDocsRender.ts +++ b/code/lib/preview-api/src/modules/preview-web/render/CsfDocsRender.ts @@ -10,18 +10,18 @@ import type { DocsRenderFunction } from '../docs-context/DocsRenderFunction'; import { DocsContext } from '../docs-context/DocsContext'; /** - * A TemplateDocsRender is a render of a docs entry that is rendered with (an) attached CSF file(s). + * A CsfDocsRender is a render of a docs entry that is rendered based on a CSF file. * * The expectation is the primary CSF file which is the `importPath` for the entry will * define a story which may contain the actual rendered JSX code for the template in the * `docs.page` parameter. * * Use cases: - * - Docs Page, where there is no parameter, and we fall back to the globally defined template. + * - Autodocs, where there is no story, and we fall back to the globally defined template. * - *.stories.mdx files, where the MDX compiler produces a CSF file with a `.parameter.docs.page` * parameter containing the compiled content of the MDX file. */ -export class TemplateDocsRender implements Render { +export class CsfDocsRender implements Render { public readonly type: RenderType = 'docs'; public readonly id: StoryId; @@ -81,7 +81,7 @@ export class TemplateDocsRender implements Render).story + this.story === (other as CsfDocsRender).story ); } diff --git a/code/lib/preview-api/src/modules/preview-web/render/StandaloneDocsRender.test.ts b/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.test.ts similarity index 79% rename from code/lib/preview-api/src/modules/preview-web/render/StandaloneDocsRender.test.ts rename to code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.test.ts index a5bf147acfe6..d422fe9368af 100644 --- a/code/lib/preview-api/src/modules/preview-web/render/StandaloneDocsRender.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.test.ts @@ -1,9 +1,9 @@ import { Channel } from '@storybook/channels'; -import type { Renderer, StandaloneDocsIndexEntry } from '@storybook/types'; +import type { Renderer, DocsIndexEntry } from '@storybook/types'; import type { StoryStore } from '../../store'; import { PREPARE_ABORTED } from './Render'; -import { StandaloneDocsRender } from './StandaloneDocsRender'; +import { MdxDocsRender } from './MdxDocsRender'; const entry = { type: 'docs', @@ -12,8 +12,7 @@ const entry = { title: 'Introduction', importPath: './Introduction.mdx', storiesImports: [], - standalone: true, -} as StandaloneDocsIndexEntry; +} as DocsIndexEntry; const createGate = (): [Promise, (_?: any) => void] => { let openGate = (_?: any) => {}; @@ -23,7 +22,7 @@ const createGate = (): [Promise, (_?: any) => void] => { return [gate, openGate]; }; -describe('StandaloneDocsRender', () => { +describe('MdxDocsRender', () => { it('throws PREPARE_ABORTED if torndown during prepare', async () => { const [importGate, openImportGate] = createGate(); const mockStore = { @@ -33,7 +32,7 @@ describe('StandaloneDocsRender', () => { }), }; - const render = new StandaloneDocsRender( + const render = new MdxDocsRender( new Channel(), mockStore as unknown as StoryStore, entry diff --git a/code/lib/preview-api/src/modules/preview-web/render/StandaloneDocsRender.ts b/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.ts similarity index 89% rename from code/lib/preview-api/src/modules/preview-web/render/StandaloneDocsRender.ts rename to code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.ts index 70019790920d..c159962a77fa 100644 --- a/code/lib/preview-api/src/modules/preview-web/render/StandaloneDocsRender.ts +++ b/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.ts @@ -10,15 +10,16 @@ import type { DocsRenderFunction } from '../docs-context/DocsRenderFunction'; import { DocsContext } from '../docs-context/DocsContext'; /** - * A StandaloneDocsRender is a render of a docs entry that doesn't directly come from a CSF file. + * A MdxDocsRender is a render of a docs entry that comes from a true MDX file, + * that is a `.mdx` file that doesn't get compiled to a CSF file. * - * A standalone render can reference zero or more CSF files that contain stories. + * A MDX render can reference (import) zero or more CSF files that contain stories. * * Use cases: * - *.mdx file that may or may not reference a specific CSF file with `` */ -export class StandaloneDocsRender implements Render { +export class MdxDocsRender implements Render { public readonly type: RenderType = 'docs'; public readonly id: StoryId; @@ -64,7 +65,7 @@ export class StandaloneDocsRender implements Render< return !!( this.id === other.id && this.exports && - this.exports === (other as StandaloneDocsRender).exports + this.exports === (other as MdxDocsRender).exports ); } diff --git a/code/lib/preview-api/src/modules/store/StoryStore.test.ts b/code/lib/preview-api/src/modules/store/StoryStore.test.ts index 3bbf2f970906..3e71abb50abe 100644 --- a/code/lib/preview-api/src/modules/store/StoryStore.test.ts +++ b/code/lib/preview-api/src/modules/store/StoryStore.test.ts @@ -658,12 +658,11 @@ describe('StoryStore', () => { }); it('does not include (modern) docs entries ever', async () => { - const docsOnlyStoryIndex: StoryIndex = { + const unnattachedStoryIndex: StoryIndex = { v: 4, entries: { ...storyIndex.entries, 'introduction--docs': { - standalone: true, type: 'docs', id: 'introduction--docs', title: 'Introduction', @@ -676,7 +675,7 @@ describe('StoryStore', () => { const store = new StoryStore(); store.setProjectAnnotations(projectAnnotations); store.initialize({ - storyIndex: docsOnlyStoryIndex, + storyIndex: unnattachedStoryIndex, importFn, cache: false, }); diff --git a/code/lib/types/src/modules/storyIndex.ts b/code/lib/types/src/modules/storyIndex.ts index 8cfc1c6f2148..3c2caec54482 100644 --- a/code/lib/types/src/modules/storyIndex.ts +++ b/code/lib/types/src/modules/storyIndex.ts @@ -61,14 +61,8 @@ export type StoryIndexEntry = BaseIndexEntry & { export type DocsIndexEntry = BaseIndexEntry & { storiesImports: Path[]; type: 'docs'; - standalone: boolean; }; -/** A StandaloneDocsIndexExtry represents a file who's default export is directly renderable */ -export type StandaloneDocsIndexEntry = DocsIndexEntry & { standalone: true }; -/** A TemplateDocsIndexEntry represents a stories file that gets rendered in "docs" mode */ -export type TemplateDocsIndexEntry = DocsIndexEntry & { standalone: false }; - export type IndexEntry = StoryIndexEntry | DocsIndexEntry; export interface V2CompatIndexEntry extends Omit { diff --git a/code/ui/blocks/src/blocks/Meta.tsx b/code/ui/blocks/src/blocks/Meta.tsx index 69930b07c93b..dc3751e711aa 100644 --- a/code/ui/blocks/src/blocks/Meta.tsx +++ b/code/ui/blocks/src/blocks/Meta.tsx @@ -21,7 +21,7 @@ export const Meta: FC = ({ of }) => { const primary = context.storyById(); return ; } catch (err) { - // It is possible to use in a standalone entry without referencing any story file + // It is possible to use in a unnattached MDX file return null; } };