Skip to content

Commit

Permalink
Merge pull request #25328 from storybookjs/shilman/autodocs-filter
Browse files Browse the repository at this point in the history
UI: Add configurable tags-based exclusion from sidebar/autodocs
  • Loading branch information
shilman authored Jan 12, 2024
2 parents 176fd77 + d9caeb8 commit ee06c80
Show file tree
Hide file tree
Showing 27 changed files with 215 additions and 13 deletions.
19 changes: 19 additions & 0 deletions code/addons/docs/src/preview.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import type { PreparedStory } from '@storybook/types';
import { global } from '@storybook/global';

const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce((acc, entry) => {
const [tag, option] = entry;
if ((option as any).excludeFromDocsStories) {
acc[tag] = true;
}
return acc;
}, {} as Record<string, boolean>);

export const parameters: any = {
docs: {
renderer: async () => {
const { DocsRenderer } = (await import('./DocsRenderer')) as any;
return new DocsRenderer();
},
stories: {
filter: (story: PreparedStory) => {
const tags = story.tags || [];
return (
tags.filter((tag) => excludeTags[tag]).length === 0 && !story.parameters.docs?.disable
);
},
},
},
};
2 changes: 2 additions & 0 deletions code/addons/docs/src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ declare module 'sveltedoc-parser' {
declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features'];

declare var LOGLEVEL: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;

declare var TAGS_OPTIONS: import('@storybook/types').TagsOptions;
4 changes: 4 additions & 0 deletions code/builders/builder-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({
title,
logLevel,
docsOptions,
tagsOptions,
} = await getData(options);

yield;
Expand Down Expand Up @@ -175,6 +176,7 @@ const starter: StarterFunction = async function* starterGeneratorFn({
refs,
logLevel,
docsOptions,
tagsOptions,
options
);

Expand Down Expand Up @@ -222,6 +224,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
title,
logLevel,
docsOptions,
tagsOptions,
} = await getData(options);
yield;

Expand Down Expand Up @@ -262,6 +265,7 @@ const builder: BuilderFunction = async function* builderGeneratorFn({ startTime,
refs,
logLevel,
docsOptions,
tagsOptions,
options
);

Expand Down
2 changes: 2 additions & 0 deletions code/builders/builder-manager/src/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const getData = async (options: Options) => {
const logLevel = options.presets.apply<string>('logLevel');
const title = options.presets.apply<string>('title');
const docsOptions = options.presets.apply('docs', {});
const tagsOptions = options.presets.apply('tags', {});
const template = readTemplate('template.ejs');
const customHead = options.presets.apply<string>('managerHead');

Expand All @@ -35,5 +36,6 @@ export const getData = async (options: Options) => {
config,
logLevel,
favicon,
tagsOptions,
};
};
4 changes: 3 additions & 1 deletion code/builders/builder-manager/src/utils/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from 'fs-extra';

import { render } from 'ejs';

import type { DocsOptions, Options, Ref } from '@storybook/types';
import type { DocsOptions, TagsOptions, Options, Ref } from '@storybook/types';

export const getTemplatePath = async (template: string) => {
return join(
Expand Down Expand Up @@ -34,6 +34,7 @@ export const renderHTML = async (
refs: Promise<Record<string, Ref>>,
logLevel: Promise<string>,
docsOptions: Promise<DocsOptions>,
tagsOptions: Promise<TagsOptions>,
{ versionCheck, previewUrl, configType, ignorePreview }: Options
) => {
const titleRef = await title;
Expand All @@ -52,6 +53,7 @@ export const renderHTML = async (
// These two need to be double stringified because the UI expects a string
VERSIONCHECK: JSON.stringify(JSON.stringify(versionCheck), null, 2),
PREVIEW_URL: JSON.stringify(previewUrl, null, 2), // global preview URL
TAGS_OPTIONS: JSON.stringify(await tagsOptions, null, 2),
},
head: (await customHead) || '',
ignorePreview,
Expand Down
1 change: 1 addition & 0 deletions code/builders/builder-vite/input/iframe.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
window.FEATURES = '[FEATURES HERE]';
window.STORIES = '[STORIES HERE]';
window.DOCS_OPTIONS = '[DOCS_OPTIONS HERE]';
window.TAGS_OPTIONS = '[TAGS_OPTIONS HERE]';

('OTHER_GLOBLALS HERE');

Expand Down
4 changes: 3 additions & 1 deletion code/builders/builder-vite/src/transform-iframe-html.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { normalizeStories } from '@storybook/core-common';
import type { DocsOptions, Options } from '@storybook/types';
import type { DocsOptions, TagsOptions, Options } from '@storybook/types';

export type PreviewHtml = string | undefined;

Expand All @@ -11,6 +11,7 @@ export async function transformIframeHtml(html: string, options: Options) {
const bodyHtmlSnippet = await presets.apply<PreviewHtml>('previewBody');
const logLevel = await presets.apply('logLevel', undefined);
const docsOptions = await presets.apply<DocsOptions>('docs');
const tagsOptions = await presets.apply<TagsOptions>('tags');

const coreOptions = await presets.apply('core');
const stories = normalizeStories(await options.presets.apply('stories', [], options), {
Expand Down Expand Up @@ -42,6 +43,7 @@ export async function transformIframeHtml(html: string, options: Options) {
.replace(`'[FEATURES HERE]'`, JSON.stringify(features || {}))
.replace(`'[STORIES HERE]'`, JSON.stringify(stories || {}))
.replace(`'[DOCS_OPTIONS HERE]'`, JSON.stringify(docsOptions || {}))
.replace(`'[TAGS_OPTIONS HERE]'`, JSON.stringify(tagsOptions || {}))
.replace('<!-- [HEAD HTML SNIPPET HERE] -->', headHtmlSnippet || '')
.replace('<!-- [BODY HTML SNIPPET HERE] -->', bodyHtmlSnippet || '');
}
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default async (
nonNormalizedStories,
modulesCount = 1000,
build,
tagsOptions,
] = await Promise.all([
presets.apply('core'),
presets.apply('frameworkOptions'),
Expand All @@ -95,6 +96,7 @@ export default async (
presets.apply('stories', []),
options.cache?.get('modulesCount').catch(() => {}),
options.presets.apply('build'),
presets.apply('tags', {}),
]);

const stories = normalizeStories(nonNormalizedStories, {
Expand Down Expand Up @@ -184,6 +186,7 @@ export default async (
importPathMatcher: specifier.importPathMatcher.source,
})),
DOCS_OPTIONS: docsOptions,
TAGS_OPTIONS: tagsOptions,
...(build?.test?.disableBlocks ? { __STORYBOOK_BLOCKS_EMPTY_MODULE__: {} } : {}),
},
headHtmlSnippet,
Expand Down
62 changes: 62 additions & 0 deletions code/e2e-tests/tags.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { test, expect } from '@playwright/test';
import { SbPage } from './util';

const storybookUrl = process.env.STORYBOOK_URL || 'http://localhost:8001';

test.describe('tags', () => {
test.beforeEach(async ({ page }) => {
await page.goto(storybookUrl);
await new SbPage(page).waitUntilLoaded();
});

test('should correctly filter dev-only, docs-only, test-only stories', async ({ page }) => {
const sbPage = new SbPage(page);

await sbPage.navigateToStory('lib/preview-api/tags', 'docs');

// Sidebar should include dev-only and exclude docs-only and test-only
const devOnlyEntry = await page.locator('#lib-preview-api-tags--dev-only').all();
expect(devOnlyEntry.length).toBe(1);

const docsOnlyEntry = await page.locator('#lib-preview-api-tags--docs-only').all();
expect(docsOnlyEntry.length).toBe(0);

const testOnlyEntry = await page.locator('#lib-preview-api-tags--test-only').all();
expect(testOnlyEntry.length).toBe(0);

// Autodocs should include docs-only and exclude dev-only and test-only
const root = sbPage.previewRoot();

const devOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--dev-only').all();
expect(devOnlyAnchor.length).toBe(0);

const docsOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--docs-only').all();
expect(docsOnlyAnchor.length).toBe(1);

const testOnlyAnchor = await root.locator('#anchor--lib-preview-api-tags--test-only').all();
expect(testOnlyAnchor.length).toBe(0);
});

test('should correctly filter out test-only autodocs pages', async ({ page }) => {
const sbPage = new SbPage(page);

await sbPage.selectToolbar('#lib-preview-api');

// Sidebar should exclude test-only stories and their docs
const componentEntry = await page.locator('#lib-preview-api-test-only-tag').all();
expect(componentEntry.length).toBe(0);

// Even though test-only autodocs not sidebar, it is still in the preview
// Even though the test-only story is filtered out of the stories, it is still the primary story (should it be?)
await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'docs');
await sbPage.waitUntilLoaded();
const docsButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' });
await expect(docsButton).toBeVisible();

// Even though test-only story not sidebar, it is still in the preview
await sbPage.deepLinkToStory(storybookUrl, 'lib/preview-api/test-only-tag', 'default');
await sbPage.waitUntilLoaded();
const storyButton = await sbPage.previewRoot().locator('button', { hasText: 'Button' });
await expect(storyButton).toBeVisible();
});
});
3 changes: 3 additions & 0 deletions code/e2e-tests/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class SbPage {
const storyLinkId = `${titleId}--${storyId}`;
const viewMode = name === 'docs' ? 'docs' : 'story';
await this.page.goto(`${baseURL}/?path=/${viewMode}/${storyLinkId}`);

await this.page.waitForURL((url) => url.search.includes(`path=/${viewMode}/${storyLinkId}`));
await this.previewRoot();
}

/**
Expand Down
2 changes: 2 additions & 0 deletions code/lib/core-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@storybook/docs-mdx": "3.0.0",
"@storybook/global": "^5.0.0",
"@storybook/manager": "workspace:*",
"@storybook/manager-api": "workspace:*",
"@storybook/node-logger": "workspace:*",
"@storybook/preview-api": "workspace:*",
"@storybook/telemetry": "workspace:*",
Expand Down Expand Up @@ -115,6 +116,7 @@
"entries": [
"./src/index.ts",
"./src/presets/common-preset.ts",
"./src/presets/common-manager.ts",
"./src/presets/common-override-preset.ts"
],
"platform": "node"
Expand Down
21 changes: 21 additions & 0 deletions code/lib/core-server/src/presets/common-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { addons } from '@storybook/manager-api';
import { global } from '@storybook/global';

const STATIC_FILTER = 'static-filter';

addons.register(STATIC_FILTER, (api) => {
// FIXME: this ensures the filter is applied after the first render
// to avoid a strange race condition in Webkit only.
const excludeTags = Object.entries(global.TAGS_OPTIONS ?? {}).reduce((acc, entry) => {
const [tag, option] = entry;
if ((option as any).excludeFromSidebar) {
acc[tag] = true;
}
return acc;
}, {} as Record<string, boolean>);

api.experimental_setFilter(STATIC_FILTER, (item) => {
const tags = item.tags || [];
return tags.filter((tag) => excludeTags[tag]).length === 0;
});
});
16 changes: 16 additions & 0 deletions code/lib/core-server/src/presets/common-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,19 @@ export const resolvedReact = async (existing: any) => {
return existing;
}
};

/**
* Set up `dev-only`, `docs-only`, `test-only` tags out of the box
*/
export const tags = async (existing: any) => {
return {
...existing,
'dev-only': { excludeFromDocsStories: true },
'docs-only': { excludeFromSidebar: true },
'test-only': { excludeFromSidebar: true, excludeFromDocsStories: true },
};
};

export const managerEntries = async (existing: any, options: Options) => {
return [require.resolve('./common-manager'), ...(existing || [])];
};
1 change: 1 addition & 0 deletions code/lib/core-server/src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ declare module '@discoveryjs/json-ext';
declare module 'watchpack';

declare var FEATURES: import('@storybook/types').StorybookConfigRaw['features'];
declare var TAGS_OPTIONS: import('@storybook/types').TagsOptions;
5 changes: 3 additions & 2 deletions code/lib/core-server/src/utils/StoryIndexGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,15 @@ export class StoryIndexGenerator {
const name = this.options.docs.defaultName ?? 'Docs';
const { metaId } = indexInputs[0];
const { title } = entries[0];
const tags = indexInputs[0].tags || [];
const metaTags = indexInputs[0].metaTags || [];
const id = toId(metaId ?? title, name);
entries.unshift({
id,
title,
name,
importPath,
type: 'docs',
tags: [...tags, 'docs', ...(!hasAutodocsTag && !isStoriesMdx ? [AUTODOCS_TAG] : [])],
tags: [...metaTags, 'docs', ...(!hasAutodocsTag && !isStoriesMdx ? [AUTODOCS_TAG] : [])],
storiesImports: [],
});
}
Expand Down Expand Up @@ -432,6 +432,7 @@ export class StoryIndexGenerator {
importPath,
storiesImports: sortedDependencies.map((dep) => dep.entries[0].importPath),
type: 'docs',
// FIXME: update this to use the index entry's metaTags once we update this to run on `IndexInputs`
tags: [...(result.tags || []), csfEntry ? 'attached-mdx' : 'unattached-mdx', 'docs'],
};
return docsEntry;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,6 @@ describe('docs entries from story extraction', () => {
"name": "docs",
"storiesImports": [],
"tags": [
"story-tag-from-indexer",
"docs",
"autodocs",
],
Expand Down Expand Up @@ -466,8 +465,6 @@ describe('docs entries from story extraction', () => {
"name": "docs",
"storiesImports": [],
"tags": [
"autodocs",
"story-tag-from-indexer",
"docs",
],
"title": "A",
Expand Down Expand Up @@ -577,8 +574,6 @@ describe('docs entries from story extraction', () => {
"name": "docs",
"storiesImports": [],
"tags": [
"stories-mdx",
"story-tag-from-indexer",
"docs",
],
"title": "A",
Expand Down
10 changes: 10 additions & 0 deletions code/lib/csf-tools/src/CsfFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,8 @@ describe('CsfFile', () => {
- component-tag
- story-tag
- play-fn
metaTags: &ref_0
- component-tag
__id: component-id--a
- type: story
importPath: foo/bar.stories.js
Expand All @@ -1109,6 +1111,7 @@ describe('CsfFile', () => {
- component-tag
- story-tag
- play-fn
metaTags: *ref_0
__id: component-id--b
`);
});
Expand Down Expand Up @@ -1138,6 +1141,8 @@ describe('CsfFile', () => {
metaId: component-id
tags:
- component-tag
metaTags:
- component-tag
__id: custom-story-id
`);
});
Expand Down Expand Up @@ -1169,6 +1174,11 @@ describe('CsfFile', () => {
- inherit-tag-dup
- story-tag
- story-tag-dup
metaTags:
- component-tag
- component-tag-dup
- component-tag-dup
- inherit-tag-dup
__id: custom-foo-title--a
`);
});
Expand Down
1 change: 1 addition & 0 deletions code/lib/csf-tools/src/CsfFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,7 @@ export class CsfFile {
title: this.meta?.title,
metaId: this.meta?.id,
tags,
metaTags: this.meta?.tags,
__id: story.id,
};
});
Expand Down
Loading

0 comments on commit ee06c80

Please sign in to comment.