Add docs option and use to drive the indexer

This commit is contained in:
Tom Coleman 2022-06-26 22:02:06 +10:00
parent 49bdd7b434
commit fea109aa1f
9 changed files with 129 additions and 29 deletions

View File

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

View File

@ -23,6 +23,11 @@ const config: StorybookConfig = {
'@storybook/addon-storyshots',
'@storybook/addon-a11y',
],
docs: {
// enabled: false,
defaultName: 'Info',
// docsPage: false,
},
typescript: {
check: true,
checkOptions: {},

View File

@ -286,6 +286,21 @@ export type Entry = string;
type StorybookRefs = Record<string, { title: string; url: string } | { disable: boolean }>;
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.
*/
@ -413,6 +428,11 @@ export interface StorybookConfig {
* Process CSF files for the story index.
*/
storyIndexers?: (indexers: StoryIndexer[], options: Options) => StoryIndexer[];
/**
* Docs related features in index generation
*/
docs?: DocsOptions;
}
export type PresetProperty<K, TStorybookConfig = StorybookConfig> =

View File

@ -13,6 +13,7 @@ import type {
Options,
StorybookConfig,
CoreConfig,
DocsOptions,
} from '@storybook/core-common';
import {
loadAllPresets,
@ -89,12 +90,13 @@ export async function buildStaticStandalone(
...options,
});
const [features, core, staticDirs, storyIndexers, stories] = await Promise.all([
const [features, core, staticDirs, storyIndexers, stories, docsOptions] = await Promise.all([
presets.apply<StorybookConfig['features']>('features'),
presets.apply<CoreConfig>('core'),
presets.apply<StorybookConfig['staticDirs']>('staticDirs'),
presets.apply('storyIndexers', []),
presets.apply('stories'),
presets.apply<DocsOptions>('docs', {}),
]);
const fullOptions: Options = {
@ -142,10 +144,10 @@ export async function buildStaticStandalone(
workingDir,
};
const normalizedStories = normalizeStories(stories, directories);
const generator = new StoryIndexGenerator(normalizedStories, {
...directories,
storyIndexers,
docs: docsOptions,
storiesV2Compatibility: !features?.breakingChangesV7 && !features?.storyStoreV7,
storyStoreV7: !!features?.storyStoreV7,
});

View File

@ -186,7 +186,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`),

View File

@ -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<DocsOptions>('docs', {});
const generator = new StoryIndexGenerator(normalizedStories, {
...directories,
storyIndexers,
docs: docsOptions,
workingDir,
storiesV2Compatibility: !features?.breakingChangesV7 && !features?.storyStoreV7,
storyStoreV7: features?.storyStoreV7,

View File

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

View File

@ -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') {

View File

@ -61,6 +61,7 @@ const getInitializedStoryIndexGenerator = async (
workingDir,
storiesV2Compatibility: false,
storyStoreV7: true,
docs: { enabled: true, defaultName: 'docs', docsPage: true },
...overrides,
});
await generator.initialize();