From b0873ad623a3fd54a55ea47e7e351f2492bf1ea8 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Sun, 26 Jun 2022 22:02:06 +1000 Subject: [PATCH] Add `docs` option and use to drive the indexer --- addons/docs/src/preset.ts | 13 +++- examples/react-ts/.storybook/main.ts | 5 ++ lib/core-common/src/types.ts | 20 +++++ lib/core-server/src/build-static.ts | 3 + lib/core-server/src/core-presets.test.ts | 1 - lib/core-server/src/dev-server.ts | 12 ++- .../src/utils/StoryIndexGenerator.test.ts | 75 +++++++++++++++---- .../src/utils/StoryIndexGenerator.ts | 25 +++++-- .../src/utils/stories-json.test.ts | 1 + 9 files changed, 128 insertions(+), 27 deletions(-) diff --git a/addons/docs/src/preset.ts b/addons/docs/src/preset.ts index cf6c7751859d..583431ebf3ef 100644 --- a/addons/docs/src/preset.ts +++ b/addons/docs/src/preset.ts @@ -3,7 +3,7 @@ import remarkSlug from 'remark-slug'; import remarkExternalLinks from 'remark-external-links'; import global from 'global'; -import type { IndexerOptions, Options, StoryIndexer } from '@storybook/core-common'; +import type { DocsOptions, IndexerOptions, Options, StoryIndexer } from '@storybook/core-common'; import { logger } from '@storybook/node-logger'; import { loadCsf } from '@storybook/csf-tools'; @@ -137,7 +137,7 @@ export async function webpack( return result; } -export const storyIndexers = async (indexers?: StoryIndexer[]) => { +export const storyIndexers = async (indexers: StoryIndexer[] | null) => { const mdxIndexer = async (fileName: string, opts: IndexerOptions) => { let code = (await fs.readFile(fileName, 'utf-8')).toString(); // @ts-ignore @@ -155,3 +155,12 @@ export const storyIndexers = async (indexers?: StoryIndexer[]) => { ...(indexers || []), ]; }; + +export const docs = (docsOptions: DocsOptions) => { + return { + ...docsOptions, + enabled: true, + defaultName: 'Docs', + docsPage: true, + }; +}; diff --git a/examples/react-ts/.storybook/main.ts b/examples/react-ts/.storybook/main.ts index 62b4d74204d9..0ac0f2cfc61f 100644 --- a/examples/react-ts/.storybook/main.ts +++ b/examples/react-ts/.storybook/main.ts @@ -23,6 +23,11 @@ const config: StorybookConfig = { '@storybook/addon-storyshots', '@storybook/addon-a11y', ], + docs: { + // enabled: false, + defaultName: 'Info', + // docsPage: false, + }, typescript: { check: true, checkOptions: {}, diff --git a/lib/core-common/src/types.ts b/lib/core-common/src/types.ts index 7674a3e7b391..480ad7d4e9ae 100644 --- a/lib/core-common/src/types.ts +++ b/lib/core-common/src/types.ts @@ -307,6 +307,21 @@ export type Entry = string; type StorybookRefs = Record; +export type DocsOptions = { + /** + * Should we generate docs entries at all under any circumstances? (i.e. can they be rendered) + */ + enabled?: boolean; + /** + * What should we call the generated docs entries? + */ + defaultName?: string; + /** + * Should we generate a docs entry per CSF file? + */ + docsPage?: boolean; +}; + /** * The interface for Storybook configuration in `main.ts` files. */ @@ -434,4 +449,9 @@ export interface StorybookConfig { * Process CSF files for the story index. */ storyIndexers?: (indexers: StoryIndexer[], options: Options) => StoryIndexer[]; + + /** + * Docs related features in index generation + */ + docs?: DocsOptions; } diff --git a/lib/core-server/src/build-static.ts b/lib/core-server/src/build-static.ts index 1ccd9abe05e6..30fbf2a53a16 100644 --- a/lib/core-server/src/build-static.ts +++ b/lib/core-server/src/build-static.ts @@ -14,6 +14,7 @@ import type { Options, StorybookConfig, CoreConfig, + DocsOptions, } from '@storybook/core-common'; import { loadAllPresets, @@ -128,10 +129,12 @@ export async function buildStaticStandalone( }; const normalizedStories = normalizeStories(await presets.apply('stories'), directories); const storyIndexers = await presets.apply('storyIndexers', []); + const docsOptions = await presets.apply('docs', {}); const generator = new StoryIndexGenerator(normalizedStories, { ...directories, storyIndexers, + docs: docsOptions, storiesV2Compatibility: !features?.breakingChangesV7 && !features?.storyStoreV7, storyStoreV7: !!features?.storyStoreV7, }); diff --git a/lib/core-server/src/core-presets.test.ts b/lib/core-server/src/core-presets.test.ts index 73d771493107..82e4b9b9cb19 100644 --- a/lib/core-server/src/core-presets.test.ts +++ b/lib/core-server/src/core-presets.test.ts @@ -172,7 +172,6 @@ describe.each([ ['prod', buildStaticStandalone], ['dev', buildDevStandalone], ])('%s', async (mode, builder) => { - console.log('running for ', mode, builder); const options = { ...baseOptions, configDir: path.resolve(`${__dirname}/../../../examples/${example}/.storybook`), diff --git a/lib/core-server/src/dev-server.ts b/lib/core-server/src/dev-server.ts index cb508868ae92..60359cfb53bd 100644 --- a/lib/core-server/src/dev-server.ts +++ b/lib/core-server/src/dev-server.ts @@ -1,8 +1,14 @@ import express, { Router } from 'express'; import compression from 'compression'; -import type { CoreConfig, Options, StorybookConfig } from '@storybook/core-common'; -import { normalizeStories, logConfig } from '@storybook/core-common'; +import { + CoreConfig, + DocsOptions, + Options, + StorybookConfig, + normalizeStories, + logConfig, +} from '@storybook/core-common'; import { telemetry } from '@storybook/telemetry'; import { getMiddleware } from './utils/middleware'; @@ -44,10 +50,12 @@ export async function storybookDevServer(options: Options) { directories ); const storyIndexers = await options.presets.apply('storyIndexers', []); + const docsOptions = await options.presets.apply('docs', {}); const generator = new StoryIndexGenerator(normalizedStories, { ...directories, storyIndexers, + docs: docsOptions, workingDir, storiesV2Compatibility: !features?.breakingChangesV7 && !features?.storyStoreV7, storyStoreV7: features?.storyStoreV7, diff --git a/lib/core-server/src/utils/StoryIndexGenerator.test.ts b/lib/core-server/src/utils/StoryIndexGenerator.test.ts index 243811f8493c..a8e5be950184 100644 --- a/lib/core-server/src/utils/StoryIndexGenerator.test.ts +++ b/lib/core-server/src/utils/StoryIndexGenerator.test.ts @@ -44,6 +44,7 @@ const options = { storyIndexers: [{ test: /\.stories\..*$/, indexer: csfIndexer }], storiesV2Compatibility: false, storyStoreV7: true, + docs: { enabled: true, defaultName: 'docs', docsPage: true }, }; describe('StoryIndexGenerator', () => { @@ -175,16 +176,15 @@ describe('StoryIndexGenerator', () => { }); describe('docs specifier', () => { + const storiesSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( + './src/A.stories.(ts|js|jsx)', + options + ); + const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( + './src/**/*.mdx', + options + ); it('extracts stories from the right files', async () => { - const storiesSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( - './src/A.stories.(ts|js|jsx)', - options - ); - const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( - './src/**/*.mdx', - options - ); - const generator = new StoryIndexGenerator([storiesSpecifier, docsSpecifier], options); await generator.initialize(); @@ -231,16 +231,63 @@ describe('StoryIndexGenerator', () => { }); it('errors when docs dependencies are missing', async () => { - const docsSpecifier: NormalizedStoriesSpecifier = normalizeStoriesEntry( - './src/**/MetaOf.mdx', - options - ); - const generator = new StoryIndexGenerator([docsSpecifier], options); await expect(() => generator.initialize()).rejects.toThrowErrorMatchingInlineSnapshot( `"Could not find \\"../A.stories\\" for docs file \\"src/docs2/MetaOf.mdx\\"."` ); }); + + it('Allows you to override default name for docs files', async () => { + const generator = new StoryIndexGenerator([storiesSpecifier, docsSpecifier], { + ...options, + docs: { + ...options.docs, + defaultName: 'Info', + }, + }); + await generator.initialize(); + + expect(await generator.getIndex()).toMatchInlineSnapshot(` + Object { + "entries": Object { + "a--info": Object { + "id": "a--info", + "importPath": "./src/docs2/MetaOf.mdx", + "name": "Info", + "storiesImports": Array [ + "./src/A.stories.js", + ], + "title": "A", + "type": "docs", + }, + "a--story-one": Object { + "id": "a--story-one", + "importPath": "./src/A.stories.js", + "name": "Story One", + "title": "A", + "type": "story", + }, + "docs2-notitle--info": Object { + "id": "docs2-notitle--info", + "importPath": "./src/docs2/NoTitle.mdx", + "name": "Info", + "storiesImports": Array [], + "title": "docs2/NoTitle", + "type": "docs", + }, + "docs2-yabbadabbadooo--info": Object { + "id": "docs2-yabbadabbadooo--info", + "importPath": "./src/docs2/Title.mdx", + "name": "Info", + "storiesImports": Array [], + "title": "docs2/Yabbadabbadooo", + "type": "docs", + }, + }, + "v": 4, + } + `); + }); }); }); diff --git a/lib/core-server/src/utils/StoryIndexGenerator.ts b/lib/core-server/src/utils/StoryIndexGenerator.ts index 411df02c848d..b845adc4fb24 100644 --- a/lib/core-server/src/utils/StoryIndexGenerator.ts +++ b/lib/core-server/src/utils/StoryIndexGenerator.ts @@ -16,6 +16,7 @@ import type { StoryIndexer, IndexerOptions, NormalizedStoriesSpecifier, + DocsOptions, } from '@storybook/core-common'; import { normalizeStoryPath } from '@storybook/core-common'; import { logger } from '@storybook/node-logger'; @@ -56,6 +57,7 @@ export class StoryIndexGenerator { storiesV2Compatibility: boolean; storyStoreV7: boolean; storyIndexers: StoryIndexer[]; + docs: DocsOptions; } ) { this.specifierToCache = new Map(); @@ -120,9 +122,12 @@ export class StoryIndexGenerator { await this.updateExtracted(async (specifier, absolutePath) => this.isDocsMdx(absolutePath) ? false : this.extractStories(specifier, absolutePath) ); - await this.updateExtracted(async (specifier, absolutePath) => - this.extractDocs(specifier, absolutePath) - ); + + if (this.options.docs.enabled) { + await this.updateExtracted(async (specifier, absolutePath) => + this.extractDocs(specifier, absolutePath) + ); + } return this.specifiers.flatMap((specifier) => { const cache = this.specifierToCache.get(specifier); @@ -217,7 +222,7 @@ export class StoryIndexGenerator { }); const title = userOrAutoTitleFromSpecifier(importPath, specifier, result.title || ofTitle); - const name = 'docs'; + const name = this.options.docs.defaultName; const id = toId(title, name); const docsEntry: DocsCacheEntry = { @@ -254,10 +259,14 @@ export class StoryIndexGenerator { const csf = await this.index(absolutePath, { makeTitle }); csf.stories.forEach(({ id, name, parameters }) => { const base = { id, title: csf.meta.title, name, importPath }; - const entry: IndexEntry = parameters?.docsOnly - ? { ...base, type: 'docs', storiesImports: [], legacy: true } - : { ...base, type: 'story' }; - entries.push(entry); + + if (parameters?.docsOnly) { + if (this.options.docs.enabled) { + entries.push({ ...base, type: 'docs', storiesImports: [], legacy: true }); + } + } else { + entries.push({ ...base, type: 'story' }); + } }); } catch (err) { if (err.name === 'NoMetaError') { diff --git a/lib/core-server/src/utils/stories-json.test.ts b/lib/core-server/src/utils/stories-json.test.ts index 91f49904d93e..a1af8223a897 100644 --- a/lib/core-server/src/utils/stories-json.test.ts +++ b/lib/core-server/src/utils/stories-json.test.ts @@ -61,6 +61,7 @@ const getInitializedStoryIndexGenerator = async ( workingDir, storiesV2Compatibility: false, storyStoreV7: true, + docs: { enabled: true, defaultName: 'docs', docsPage: true }, ...overrides, }); await generator.initialize();