From 32544d3d5040c6dbca8a95f09b8cecedc67de0f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:36:44 +0000 Subject: [PATCH 01/11] Initial plan From 33f4ebd75638fcf7b33fa3d362b4d96edab92e33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:49:27 +0000 Subject: [PATCH 02/11] feat(addon-docs): add MDX manifest generation - Create manifest.ts to generate manifests for MDX files - Filter MDX entries with 'attached-mdx' or 'unattached-mdx' tags - Export experimental_manifests from preset.ts - Add comprehensive tests for MDX manifest generation Co-authored-by: JReinhold <5678122+JReinhold@users.noreply.github.com> --- code/addons/docs/src/manifest.test.ts | 224 ++++++++++++++++++++++++++ code/addons/docs/src/manifest.ts | 77 +++++++++ code/addons/docs/src/preset.ts | 1 + 3 files changed, 302 insertions(+) create mode 100644 code/addons/docs/src/manifest.test.ts create mode 100644 code/addons/docs/src/manifest.ts diff --git a/code/addons/docs/src/manifest.test.ts b/code/addons/docs/src/manifest.test.ts new file mode 100644 index 000000000000..cba8f7291ed5 --- /dev/null +++ b/code/addons/docs/src/manifest.test.ts @@ -0,0 +1,224 @@ +import { describe, expect, it } from 'vitest'; + +import type { DocsIndexEntry, IndexEntry } from 'storybook/internal/types'; + +import { experimental_manifests } from './manifest'; + +describe('experimental_manifests', () => { + it('should return existing manifests when no MDX entries are found', async () => { + const existingManifests = { components: { v: 0, components: {} } }; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--story', + name: 'Story', + title: 'Example', + type: 'story', + subtype: 'story', + importPath: './Example.stories.tsx', + tags: ['manifest'], + }, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toEqual(existingManifests); + }); + + it('should generate MDX manifest for attached-mdx entries', async () => { + const existingManifests = {}; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['manifest', 'attached-mdx'], + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toHaveProperty('mdx'); + expect(result.mdx).toEqual({ + v: 0, + entries: { + 'example--docs': { + id: 'example--docs', + name: 'docs', + path: './Example.mdx', + title: 'Example', + tags: ['manifest', 'attached-mdx'], + storiesImports: ['./Example.stories.tsx'], + }, + }, + }); + }); + + it('should generate MDX manifest for unattached-mdx entries', async () => { + const existingManifests = {}; + const manifestEntries: IndexEntry[] = [ + { + id: 'standalone--docs', + name: 'docs', + title: 'Standalone', + type: 'docs', + importPath: './Standalone.mdx', + tags: ['manifest', 'unattached-mdx'], + storiesImports: [], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toHaveProperty('mdx'); + expect(result.mdx).toEqual({ + v: 0, + entries: { + 'standalone--docs': { + id: 'standalone--docs', + name: 'docs', + path: './Standalone.mdx', + title: 'Standalone', + tags: ['manifest', 'unattached-mdx'], + storiesImports: [], + }, + }, + }); + }); + + it('should include multiple MDX entries in the manifest', async () => { + const existingManifests = {}; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['manifest', 'attached-mdx'], + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + { + id: 'standalone--docs', + name: 'docs', + title: 'Standalone', + type: 'docs', + importPath: './Standalone.mdx', + tags: ['manifest', 'unattached-mdx'], + storiesImports: [], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toHaveProperty('mdx'); + expect(result.mdx.entries).toHaveProperty('example--docs'); + expect(result.mdx.entries).toHaveProperty('standalone--docs'); + expect(Object.keys(result.mdx.entries)).toHaveLength(2); + }); + + it('should exclude MDX entries without manifest tag', async () => { + const existingManifests = {}; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['attached-mdx'], // No 'manifest' tag + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toEqual(existingManifests); + }); + + it('should exclude docs entries without attached-mdx or unattached-mdx tags', async () => { + const existingManifests = {}; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['manifest', 'autodocs'], // Has manifest but not attached/unattached-mdx + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toEqual(existingManifests); + }); + + it('should preserve existing manifests', async () => { + const existingManifests = { + components: { v: 0, components: { 'example-component': { id: 'example-component' } } }, + }; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['manifest', 'attached-mdx'], + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toHaveProperty('components'); + expect(result).toHaveProperty('mdx'); + expect(result.components).toEqual(existingManifests.components); + }); + + it('should handle entries with all tag variations', async () => { + const existingManifests = {}; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['dev', 'test', 'manifest', 'attached-mdx'], + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result.mdx.entries['example--docs'].tags).toEqual([ + 'dev', + 'test', + 'manifest', + 'attached-mdx', + ]); + }); + + it('should handle entries without tags array', async () => { + const existingManifests = {}; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + ]; + + const result = await experimental_manifests(existingManifests, { manifestEntries }); + + expect(result).toEqual(existingManifests); + }); +}); diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts new file mode 100644 index 000000000000..6267170707b5 --- /dev/null +++ b/code/addons/docs/src/manifest.ts @@ -0,0 +1,77 @@ +import { logger } from 'storybook/internal/node-logger'; +import type { + DocsIndexEntry, + IndexEntry, + PresetPropertyFn, + StorybookConfigRaw, +} from 'storybook/internal/types'; + +const ATTACHED_MDX_TAG = 'attached-mdx'; +const UNATTACHED_MDX_TAG = 'unattached-mdx'; + +interface MdxManifestEntry { + id: string; + name: string; + path: string; + title: string; + tags: string[]; + storiesImports?: string[]; +} + +interface MdxManifest { + v: number; + entries: Record; +} + +/** + * Generates a manifest of MDX documentation files. + * This extends the existing manifest system to include MDX files with the + * 'attached-mdx' or 'unattached-mdx' tags. + */ +export const experimental_manifests: PresetPropertyFn< + 'experimental_manifests', + StorybookConfigRaw, + { manifestEntries: IndexEntry[] } +> = async (existingManifests = {}, { manifestEntries }) => { + const startPerformance = performance.now(); + + // Filter for MDX docs entries that have the manifest tag + const mdxEntries = manifestEntries.filter( + (entry): entry is DocsIndexEntry => + entry.type === 'docs' && + entry.tags?.includes('manifest') === true && + (entry.tags?.includes(ATTACHED_MDX_TAG) === true || + entry.tags?.includes(UNATTACHED_MDX_TAG) === true) + ); + + if (mdxEntries.length === 0) { + logger.verbose('No MDX entries found with manifest tag for documentation manifest'); + return existingManifests; + } + + // Convert MDX entries to manifest format + const entries: Record = {}; + + for (const entry of mdxEntries) { + entries[entry.id] = { + id: entry.id, + name: entry.name, + path: entry.importPath, + title: entry.title, + tags: entry.tags || [], + storiesImports: entry.storiesImports || [], + }; + } + + logger.verbose( + `MDX manifest generation took ${performance.now() - startPerformance}ms for ${mdxEntries.length} entries` + ); + + return { + ...existingManifests, + mdx: { + v: 0, + entries, + } satisfies MdxManifest, + }; +}; diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts index ce4cab9a4ffe..d518ff71c500 100644 --- a/code/addons/docs/src/preset.ts +++ b/code/addons/docs/src/preset.ts @@ -219,3 +219,4 @@ const optimizeViteDeps = [ ]; export { webpackX as webpack, docsX as docs, optimizeViteDeps }; +export { experimental_manifests } from './manifest'; From 822d10f71ee40e34aff39e78120621eb35b3c9f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:50:54 +0000 Subject: [PATCH 03/11] style: apply prettier formatting to manifest.ts Co-authored-by: JReinhold <5678122+JReinhold@users.noreply.github.com> --- code/addons/docs/src/manifest.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts index 6267170707b5..4f9eeb0334b7 100644 --- a/code/addons/docs/src/manifest.ts +++ b/code/addons/docs/src/manifest.ts @@ -24,9 +24,8 @@ interface MdxManifest { } /** - * Generates a manifest of MDX documentation files. - * This extends the existing manifest system to include MDX files with the - * 'attached-mdx' or 'unattached-mdx' tags. + * Generates a manifest of MDX documentation files. This extends the existing manifest system to + * include MDX files with the 'attached-mdx' or 'unattached-mdx' tags. */ export const experimental_manifests: PresetPropertyFn< 'experimental_manifests', From 865d7e1bec2eabc48491a5c04a1c2a319d996903 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 10:53:53 +0000 Subject: [PATCH 04/11] fix: use Path type for consistency with DocsIndexEntry Co-authored-by: JReinhold <5678122+JReinhold@users.noreply.github.com> --- code/addons/docs/src/manifest.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts index 4f9eeb0334b7..35027270447a 100644 --- a/code/addons/docs/src/manifest.ts +++ b/code/addons/docs/src/manifest.ts @@ -2,6 +2,7 @@ import { logger } from 'storybook/internal/node-logger'; import type { DocsIndexEntry, IndexEntry, + Path, PresetPropertyFn, StorybookConfigRaw, } from 'storybook/internal/types'; @@ -12,10 +13,10 @@ const UNATTACHED_MDX_TAG = 'unattached-mdx'; interface MdxManifestEntry { id: string; name: string; - path: string; + path: Path; title: string; tags: string[]; - storiesImports?: string[]; + storiesImports: Path[]; } interface MdxManifest { @@ -58,7 +59,7 @@ export const experimental_manifests: PresetPropertyFn< path: entry.importPath, title: entry.title, tags: entry.tags || [], - storiesImports: entry.storiesImports || [], + storiesImports: entry.storiesImports, }; } From 493124dddf607fdf011fbb865fd6326f0eaf256f Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Mon, 22 Dec 2025 23:38:55 +0100 Subject: [PATCH 05/11] basic docs manifest --- code/addons/docs/src/manifest.test.ts | 215 ++++++++++++-------------- code/addons/docs/src/manifest.ts | 69 ++++----- code/addons/docs/src/preset.ts | 3 +- 3 files changed, 134 insertions(+), 153 deletions(-) diff --git a/code/addons/docs/src/manifest.test.ts b/code/addons/docs/src/manifest.test.ts index cba8f7291ed5..d0530e97c55e 100644 --- a/code/addons/docs/src/manifest.test.ts +++ b/code/addons/docs/src/manifest.test.ts @@ -1,12 +1,61 @@ -import { describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { DocsIndexEntry, IndexEntry } from 'storybook/internal/types'; -import { experimental_manifests } from './manifest'; +import { vol } from 'memfs'; + +import { manifests } from './manifest'; + +vi.mock('node:fs/promises', async () => { + const memfs = await vi.importActual('memfs'); + return memfs.fs.promises; +}); + +beforeEach(() => { + vi.spyOn(process, 'cwd').mockReturnValue('/app'); + vol.fromJSON( + { + './Example.mdx': '# Example\n\nThis is example documentation.', + './Standalone.mdx': '# Standalone\n\nThis is standalone documentation.', + }, + '/app' + ); +}); + +interface DocsManifestEntry { + id: string; + name: string; + path: string; + title: string; + content: string; +} + +interface DocsManifest { + v: number; + docs: Record; +} + +interface ManifestResult { + docs?: DocsManifest; + components?: unknown; +} describe('experimental_manifests', () => { - it('should return existing manifests when no MDX entries are found', async () => { - const existingManifests = { components: { v: 0, components: {} } }; + it('should return existing manifests when no docs entries are found', async () => { + const existingManifests = { + components: { + v: 0, + components: { + 'example-component': { + id: 'example-component', + path: './Example.stories.tsx', + name: 'Example', + stories: [], + jsDocTags: {}, + }, + }, + }, + }; const manifestEntries: IndexEntry[] = [ { id: 'example--story', @@ -19,13 +68,14 @@ describe('experimental_manifests', () => { }, ]; - const result = await experimental_manifests(existingManifests, { manifestEntries }); + const result = (await manifests(existingManifests, { + manifestEntries, + } as any)) as ManifestResult; expect(result).toEqual(existingManifests); }); - it('should generate MDX manifest for attached-mdx entries', async () => { - const existingManifests = {}; + it('should generate docs manifest for attached docs entries', async () => { const manifestEntries: IndexEntry[] = [ { id: 'example--docs', @@ -33,31 +83,29 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest', 'attached-mdx'], + tags: ['manifest'], storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, ]; - const result = await experimental_manifests(existingManifests, { manifestEntries }); + const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult; - expect(result).toHaveProperty('mdx'); - expect(result.mdx).toEqual({ + expect(result).toHaveProperty('docs'); + expect(result.docs).toEqual({ v: 0, - entries: { + docs: { 'example--docs': { id: 'example--docs', name: 'docs', path: './Example.mdx', title: 'Example', - tags: ['manifest', 'attached-mdx'], - storiesImports: ['./Example.stories.tsx'], + content: '# Example\n\nThis is example documentation.', }, }, }); }); - it('should generate MDX manifest for unattached-mdx entries', async () => { - const existingManifests = {}; + it('should generate docs manifest for unattached-mdx entries', async () => { const manifestEntries: IndexEntry[] = [ { id: 'standalone--docs', @@ -70,26 +118,24 @@ describe('experimental_manifests', () => { } satisfies DocsIndexEntry, ]; - const result = await experimental_manifests(existingManifests, { manifestEntries }); + const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult; - expect(result).toHaveProperty('mdx'); - expect(result.mdx).toEqual({ + expect(result).toHaveProperty('docs'); + expect(result.docs).toEqual({ v: 0, - entries: { + docs: { 'standalone--docs': { id: 'standalone--docs', name: 'docs', path: './Standalone.mdx', title: 'Standalone', - tags: ['manifest', 'unattached-mdx'], - storiesImports: [], + content: '# Standalone\n\nThis is standalone documentation.', }, }, }); }); - it('should include multiple MDX entries in the manifest', async () => { - const existingManifests = {}; + it('should include multiple docs entries in the manifest', async () => { const manifestEntries: IndexEntry[] = [ { id: 'example--docs', @@ -97,7 +143,7 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest', 'attached-mdx'], + tags: ['manifest'], storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, { @@ -111,55 +157,34 @@ describe('experimental_manifests', () => { } satisfies DocsIndexEntry, ]; - const result = await experimental_manifests(existingManifests, { manifestEntries }); - - expect(result).toHaveProperty('mdx'); - expect(result.mdx.entries).toHaveProperty('example--docs'); - expect(result.mdx.entries).toHaveProperty('standalone--docs'); - expect(Object.keys(result.mdx.entries)).toHaveLength(2); - }); - - it('should exclude MDX entries without manifest tag', async () => { - const existingManifests = {}; - const manifestEntries: IndexEntry[] = [ - { - id: 'example--docs', - name: 'docs', - title: 'Example', - type: 'docs', - importPath: './Example.mdx', - tags: ['attached-mdx'], // No 'manifest' tag - storiesImports: ['./Example.stories.tsx'], - } satisfies DocsIndexEntry, - ]; - - const result = await experimental_manifests(existingManifests, { manifestEntries }); - - expect(result).toEqual(existingManifests); - }); - - it('should exclude docs entries without attached-mdx or unattached-mdx tags', async () => { - const existingManifests = {}; - const manifestEntries: IndexEntry[] = [ - { - id: 'example--docs', - name: 'docs', - title: 'Example', - type: 'docs', - importPath: './Example.mdx', - tags: ['manifest', 'autodocs'], // Has manifest but not attached/unattached-mdx - storiesImports: ['./Example.stories.tsx'], - } satisfies DocsIndexEntry, - ]; - - const result = await experimental_manifests(existingManifests, { manifestEntries }); - - expect(result).toEqual(existingManifests); + const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult; + + expect(result).toHaveProperty('docs'); + expect(result.docs?.docs).toHaveProperty('example--docs'); + expect(result.docs?.docs).toHaveProperty('standalone--docs'); + expect(Object.keys(result.docs?.docs ?? {})).toHaveLength(2); + expect(result.docs?.docs['example--docs'].content).toBe( + '# Example\n\nThis is example documentation.' + ); + expect(result.docs?.docs['standalone--docs'].content).toBe( + '# Standalone\n\nThis is standalone documentation.' + ); }); it('should preserve existing manifests', async () => { const existingManifests = { - components: { v: 0, components: { 'example-component': { id: 'example-component' } } }, + components: { + v: 0, + components: { + 'example-component': { + id: 'example-component', + path: './Example.stories.tsx', + name: 'Example', + stories: [], + jsDocTags: {}, + }, + }, + }, }; const manifestEntries: IndexEntry[] = [ { @@ -168,57 +193,17 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest', 'attached-mdx'], + tags: ['manifest'], storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, ]; - const result = await experimental_manifests(existingManifests, { manifestEntries }); + const result = (await manifests(existingManifests, { + manifestEntries, + } as any)) as ManifestResult; expect(result).toHaveProperty('components'); - expect(result).toHaveProperty('mdx'); + expect(result).toHaveProperty('docs'); expect(result.components).toEqual(existingManifests.components); }); - - it('should handle entries with all tag variations', async () => { - const existingManifests = {}; - const manifestEntries: IndexEntry[] = [ - { - id: 'example--docs', - name: 'docs', - title: 'Example', - type: 'docs', - importPath: './Example.mdx', - tags: ['dev', 'test', 'manifest', 'attached-mdx'], - storiesImports: ['./Example.stories.tsx'], - } satisfies DocsIndexEntry, - ]; - - const result = await experimental_manifests(existingManifests, { manifestEntries }); - - expect(result.mdx.entries['example--docs'].tags).toEqual([ - 'dev', - 'test', - 'manifest', - 'attached-mdx', - ]); - }); - - it('should handle entries without tags array', async () => { - const existingManifests = {}; - const manifestEntries: IndexEntry[] = [ - { - id: 'example--docs', - name: 'docs', - title: 'Example', - type: 'docs', - importPath: './Example.mdx', - storiesImports: ['./Example.stories.tsx'], - } satisfies DocsIndexEntry, - ]; - - const result = await experimental_manifests(existingManifests, { manifestEntries }); - - expect(result).toEqual(existingManifests); - }); }); diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts index 35027270447a..bbb37f0cc62e 100644 --- a/code/addons/docs/src/manifest.ts +++ b/code/addons/docs/src/manifest.ts @@ -1,3 +1,6 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; + import { logger } from 'storybook/internal/node-logger'; import type { DocsIndexEntry, @@ -7,71 +10,65 @@ import type { StorybookConfigRaw, } from 'storybook/internal/types'; -const ATTACHED_MDX_TAG = 'attached-mdx'; -const UNATTACHED_MDX_TAG = 'unattached-mdx'; - -interface MdxManifestEntry { +interface DocsManifestEntry { id: string; name: string; path: Path; title: string; - tags: string[]; - storiesImports: Path[]; + content: string; } -interface MdxManifest { +interface DocsManifest { v: number; - entries: Record; + docs: Record; } /** - * Generates a manifest of MDX documentation files. This extends the existing manifest system to - * include MDX files with the 'attached-mdx' or 'unattached-mdx' tags. + * Generates a manifest of docs entries. This extends the existing manifest system to include docs + * entries. */ -export const experimental_manifests: PresetPropertyFn< +export const manifests: PresetPropertyFn< 'experimental_manifests', StorybookConfigRaw, { manifestEntries: IndexEntry[] } > = async (existingManifests = {}, { manifestEntries }) => { const startPerformance = performance.now(); - // Filter for MDX docs entries that have the manifest tag - const mdxEntries = manifestEntries.filter( - (entry): entry is DocsIndexEntry => - entry.type === 'docs' && - entry.tags?.includes('manifest') === true && - (entry.tags?.includes(ATTACHED_MDX_TAG) === true || - entry.tags?.includes(UNATTACHED_MDX_TAG) === true) + const docsEntries = manifestEntries.filter( + (entry): entry is DocsIndexEntry => entry.type === 'docs' ); - if (mdxEntries.length === 0) { - logger.verbose('No MDX entries found with manifest tag for documentation manifest'); + if (docsEntries.length === 0) { return existingManifests; } - // Convert MDX entries to manifest format - const entries: Record = {}; + const entriesWithContent = await Promise.all( + docsEntries.map(async (entry) => { + const absolutePath = path.join(process.cwd(), entry.importPath); + const content = await fs.readFile(absolutePath, 'utf-8'); + return { + id: entry.id, + name: entry.name, + path: entry.importPath, + title: entry.title, + content, + }; + }) + ); - for (const entry of mdxEntries) { - entries[entry.id] = { - id: entry.id, - name: entry.name, - path: entry.importPath, - title: entry.title, - tags: entry.tags || [], - storiesImports: entry.storiesImports, - }; - } + const docsManifests: Record = Object.fromEntries( + entriesWithContent.map((entry) => [entry.id, entry]) + ); logger.verbose( - `MDX manifest generation took ${performance.now() - startPerformance}ms for ${mdxEntries.length} entries` + `Docs manifest generation took ${performance.now() - startPerformance}ms for ${docsEntries.length} entries` ); return { ...existingManifests, - mdx: { + docs: { v: 0, - entries, - } satisfies MdxManifest, + docs: docsManifests, + } satisfies DocsManifest, }; }; diff --git a/code/addons/docs/src/preset.ts b/code/addons/docs/src/preset.ts index d518ff71c500..5aaeca9b66ed 100644 --- a/code/addons/docs/src/preset.ts +++ b/code/addons/docs/src/preset.ts @@ -3,7 +3,6 @@ import { fileURLToPath } from 'node:url'; import { logger } from 'storybook/internal/node-logger'; import type { Options, PresetProperty, StorybookConfigRaw } from 'storybook/internal/types'; -import { type CsfEnricher } from 'storybook/internal/types'; import type { CsfPluginOptions } from '@storybook/csf-plugin'; @@ -219,4 +218,4 @@ const optimizeViteDeps = [ ]; export { webpackX as webpack, docsX as docs, optimizeViteDeps }; -export { experimental_manifests } from './manifest'; +export { manifests as experimental_manifests } from './manifest'; From bf31e4b2dba751db20dcebb982b224249a2b0a14 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Mon, 22 Dec 2025 23:42:08 +0100 Subject: [PATCH 06/11] add error handling to docs manifests --- code/addons/docs/src/manifest.test.ts | 73 ++++++++++++++++++++++++++- code/addons/docs/src/manifest.ts | 34 +++++++++---- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/code/addons/docs/src/manifest.test.ts b/code/addons/docs/src/manifest.test.ts index d0530e97c55e..44c9e40657a6 100644 --- a/code/addons/docs/src/manifest.test.ts +++ b/code/addons/docs/src/manifest.test.ts @@ -27,7 +27,8 @@ interface DocsManifestEntry { name: string; path: string; title: string; - content: string; + content?: string; + error?: { name: string; message: string }; } interface DocsManifest { @@ -206,4 +207,74 @@ describe('experimental_manifests', () => { expect(result).toHaveProperty('docs'); expect(result.components).toEqual(existingManifests.components); }); + + it('should include error when file cannot be read', async () => { + const manifestEntries: IndexEntry[] = [ + { + id: 'missing--docs', + name: 'docs', + title: 'Missing', + type: 'docs', + importPath: './NonExistent.mdx', + tags: ['manifest'], + storiesImports: [], + } satisfies DocsIndexEntry, + ]; + + const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult; + + expect(result).toHaveProperty('docs'); + expect(result.docs?.docs['missing--docs']).toEqual({ + id: 'missing--docs', + name: 'docs', + path: './NonExistent.mdx', + title: 'Missing', + error: { + name: 'Error', + message: expect.stringContaining('ENOENT'), + }, + }); + expect(result.docs?.docs['missing--docs'].content).toBeUndefined(); + }); + + it('should handle mixed success and error entries', async () => { + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['manifest'], + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + { + id: 'missing--docs', + name: 'docs', + title: 'Missing', + type: 'docs', + importPath: './NonExistent.mdx', + tags: ['manifest'], + storiesImports: [], + } satisfies DocsIndexEntry, + ]; + + const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult; + + expect(result).toHaveProperty('docs'); + expect(Object.keys(result.docs?.docs ?? {})).toHaveLength(2); + + // Successful entry + expect(result.docs?.docs['example--docs'].content).toBe( + '# Example\n\nThis is example documentation.' + ); + expect(result.docs?.docs['example--docs'].error).toBeUndefined(); + + // Failed entry + expect(result.docs?.docs['missing--docs'].content).toBeUndefined(); + expect(result.docs?.docs['missing--docs'].error).toEqual({ + name: 'Error', + message: expect.stringContaining('ENOENT'), + }); + }); }); diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts index bbb37f0cc62e..d4359aacd126 100644 --- a/code/addons/docs/src/manifest.ts +++ b/code/addons/docs/src/manifest.ts @@ -15,7 +15,8 @@ interface DocsManifestEntry { name: string; path: Path; title: string; - content: string; + content?: string; + error?: { name: string; message: string }; } interface DocsManifest { @@ -43,16 +44,29 @@ export const manifests: PresetPropertyFn< } const entriesWithContent = await Promise.all( - docsEntries.map(async (entry) => { + docsEntries.map(async (entry): Promise => { const absolutePath = path.join(process.cwd(), entry.importPath); - const content = await fs.readFile(absolutePath, 'utf-8'); - return { - id: entry.id, - name: entry.name, - path: entry.importPath, - title: entry.title, - content, - }; + try { + const content = await fs.readFile(absolutePath, 'utf-8'); + return { + id: entry.id, + name: entry.name, + path: entry.importPath, + title: entry.title, + content, + }; + } catch (err) { + return { + id: entry.id, + name: entry.name, + path: entry.importPath, + title: entry.title, + error: { + name: err instanceof Error ? err.name : 'Error', + message: err instanceof Error ? err.message : String(err), + }, + }; + } }) ); From 8417c4ee221975e99e29ee82e466e1dbeffe7370 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Mon, 22 Dec 2025 23:52:18 +0100 Subject: [PATCH 07/11] unattached vs attached docs wip --- code/addons/docs/src/manifest.test.ts | 143 ++++++++++++++-------- code/addons/docs/src/manifest.ts | 165 ++++++++++++++++++++------ 2 files changed, 222 insertions(+), 86 deletions(-) diff --git a/code/addons/docs/src/manifest.test.ts b/code/addons/docs/src/manifest.test.ts index 44c9e40657a6..2d836bd48266 100644 --- a/code/addons/docs/src/manifest.test.ts +++ b/code/addons/docs/src/manifest.test.ts @@ -4,6 +4,7 @@ import type { DocsIndexEntry, IndexEntry } from 'storybook/internal/types'; import { vol } from 'memfs'; +import type { DocsManifestEntry } from './manifest'; import { manifests } from './manifest'; vi.mock('node:fs/promises', async () => { @@ -22,23 +23,28 @@ beforeEach(() => { ); }); -interface DocsManifestEntry { +interface DocsManifest { + v: number; + docs: Record; +} + +interface ComponentManifestWithDocs { id: string; - name: string; path: string; - title: string; - content?: string; - error?: { name: string; message: string }; + name: string; + stories: unknown[]; + jsDocTags: Record; + docs?: Record; } -interface DocsManifest { +interface ComponentsManifestWithDocs { v: number; - docs: Record; + components: Record; } interface ManifestResult { docs?: DocsManifest; - components?: unknown; + components?: ComponentsManifestWithDocs; } describe('experimental_manifests', () => { @@ -76,7 +82,21 @@ describe('experimental_manifests', () => { expect(result).toEqual(existingManifests); }); - it('should generate docs manifest for attached docs entries', async () => { + it('should add attached docs entries to component manifests', async () => { + const existingManifests = { + components: { + v: 0, + components: { + example: { + id: 'example', + path: './Example.stories.tsx', + name: 'Example', + stories: [], + jsDocTags: {}, + }, + }, + }, + }; const manifestEntries: IndexEntry[] = [ { id: 'example--docs', @@ -89,19 +109,19 @@ describe('experimental_manifests', () => { } satisfies DocsIndexEntry, ]; - const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult; + const result = (await manifests(existingManifests, { + manifestEntries, + } as any)) as ManifestResult; - expect(result).toHaveProperty('docs'); - expect(result.docs).toEqual({ - v: 0, - docs: { - 'example--docs': { - id: 'example--docs', - name: 'docs', - path: './Example.mdx', - title: 'Example', - content: '# Example\n\nThis is example documentation.', - }, + expect(result).toHaveProperty('components'); + expect(result).not.toHaveProperty('docs'); + expect(result.components?.components.example.docs).toEqual({ + 'example--docs': { + id: 'example--docs', + name: 'docs', + path: './Example.mdx', + title: 'Example', + content: '# Example\n\nThis is example documentation.', }, }); }); @@ -136,7 +156,21 @@ describe('experimental_manifests', () => { }); }); - it('should include multiple docs entries in the manifest', async () => { + it('should handle both attached and unattached docs entries separately', async () => { + const existingManifests = { + components: { + v: 0, + components: { + example: { + id: 'example', + path: './Example.stories.tsx', + name: 'Example', + stories: [], + jsDocTags: {}, + }, + }, + }, + }; const manifestEntries: IndexEntry[] = [ { id: 'example--docs', @@ -158,27 +192,37 @@ describe('experimental_manifests', () => { } satisfies DocsIndexEntry, ]; - const result = (await manifests(undefined, { manifestEntries } as any)) as ManifestResult; + const result = (await manifests(existingManifests, { + manifestEntries, + } as any)) as ManifestResult; + // Unattached docs should be in the docs manifest expect(result).toHaveProperty('docs'); - expect(result.docs?.docs).toHaveProperty('example--docs'); expect(result.docs?.docs).toHaveProperty('standalone--docs'); - expect(Object.keys(result.docs?.docs ?? {})).toHaveLength(2); - expect(result.docs?.docs['example--docs'].content).toBe( - '# Example\n\nThis is example documentation.' - ); + expect(Object.keys(result.docs?.docs ?? {})).toHaveLength(1); expect(result.docs?.docs['standalone--docs'].content).toBe( '# Standalone\n\nThis is standalone documentation.' ); + + // Attached docs should be in the component manifest + expect(result.components?.components.example.docs).toEqual({ + 'example--docs': { + id: 'example--docs', + name: 'docs', + path: './Example.mdx', + title: 'Example', + content: '# Example\n\nThis is example documentation.', + }, + }); }); - it('should preserve existing manifests', async () => { + it('should preserve existing manifests and add unattached docs', async () => { const existingManifests = { components: { v: 0, components: { - 'example-component': { - id: 'example-component', + example: { + id: 'example', path: './Example.stories.tsx', name: 'Example', stories: [], @@ -189,13 +233,13 @@ describe('experimental_manifests', () => { }; const manifestEntries: IndexEntry[] = [ { - id: 'example--docs', + id: 'standalone--docs', name: 'docs', - title: 'Example', + title: 'Standalone', type: 'docs', - importPath: './Example.mdx', - tags: ['manifest'], - storiesImports: ['./Example.stories.tsx'], + importPath: './Standalone.mdx', + tags: ['manifest', 'unattached-mdx'], + storiesImports: [], } satisfies DocsIndexEntry, ]; @@ -205,10 +249,11 @@ describe('experimental_manifests', () => { expect(result).toHaveProperty('components'); expect(result).toHaveProperty('docs'); - expect(result.components).toEqual(existingManifests.components); + // Components should be preserved (no docs added since no attached docs entries) + expect(result.components?.components.example.docs).toBeUndefined(); }); - it('should include error when file cannot be read', async () => { + it('should include error when file cannot be read for unattached docs', async () => { const manifestEntries: IndexEntry[] = [ { id: 'missing--docs', @@ -216,7 +261,7 @@ describe('experimental_manifests', () => { title: 'Missing', type: 'docs', importPath: './NonExistent.mdx', - tags: ['manifest'], + tags: ['manifest', 'unattached-mdx'], storiesImports: [], } satisfies DocsIndexEntry, ]; @@ -237,16 +282,16 @@ describe('experimental_manifests', () => { expect(result.docs?.docs['missing--docs'].content).toBeUndefined(); }); - it('should handle mixed success and error entries', async () => { + it('should handle mixed success and error entries for unattached docs', async () => { const manifestEntries: IndexEntry[] = [ { - id: 'example--docs', + id: 'standalone--docs', name: 'docs', - title: 'Example', + title: 'Standalone', type: 'docs', - importPath: './Example.mdx', - tags: ['manifest'], - storiesImports: ['./Example.stories.tsx'], + importPath: './Standalone.mdx', + tags: ['manifest', 'unattached-mdx'], + storiesImports: [], } satisfies DocsIndexEntry, { id: 'missing--docs', @@ -254,7 +299,7 @@ describe('experimental_manifests', () => { title: 'Missing', type: 'docs', importPath: './NonExistent.mdx', - tags: ['manifest'], + tags: ['manifest', 'unattached-mdx'], storiesImports: [], } satisfies DocsIndexEntry, ]; @@ -265,10 +310,10 @@ describe('experimental_manifests', () => { expect(Object.keys(result.docs?.docs ?? {})).toHaveLength(2); // Successful entry - expect(result.docs?.docs['example--docs'].content).toBe( - '# Example\n\nThis is example documentation.' + expect(result.docs?.docs['standalone--docs'].content).toBe( + '# Standalone\n\nThis is standalone documentation.' ); - expect(result.docs?.docs['example--docs'].error).toBeUndefined(); + expect(result.docs?.docs['standalone--docs'].error).toBeUndefined(); // Failed entry expect(result.docs?.docs['missing--docs'].content).toBeUndefined(); diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts index d4359aacd126..88358f5229b9 100644 --- a/code/addons/docs/src/manifest.ts +++ b/code/addons/docs/src/manifest.ts @@ -3,14 +3,18 @@ import * as path from 'node:path'; import { logger } from 'storybook/internal/node-logger'; import type { + ComponentManifest, DocsIndexEntry, IndexEntry, + Manifests, Path, PresetPropertyFn, StorybookConfigRaw, } from 'storybook/internal/types'; -interface DocsManifestEntry { +const UNATTACHED_MDX_TAG = 'unattached-mdx'; + +export interface DocsManifestEntry { id: string; name: string; path: Path; @@ -24,9 +28,106 @@ interface DocsManifest { docs: Record; } +interface ComponentManifestWithDocs extends ComponentManifest { + docs?: Record; +} + +interface ComponentsManifestWithDocs { + v: number; + components: Record; +} + +interface ManifestsWithDocs extends Manifests { + docs?: DocsManifest; + components?: ComponentsManifestWithDocs; +} + +/** Converts a DocsIndexEntry to a DocsManifestEntry by reading its file content. */ +export async function createDocsManifestEntry(entry: DocsIndexEntry): Promise { + const absolutePath = path.join(process.cwd(), entry.importPath); + try { + const content = await fs.readFile(absolutePath, 'utf-8'); + return { + id: entry.id, + name: entry.name, + path: entry.importPath, + title: entry.title, + content, + }; + } catch (err) { + return { + id: entry.id, + name: entry.name, + path: entry.importPath, + title: entry.title, + error: { + name: err instanceof Error ? err.name : 'Error', + message: err instanceof Error ? err.message : String(err), + }, + }; + } +} + +/** + * Processes unattached MDX entries (standalone docs not attached to any component). These are added + * to a separate `docs` manifest. + */ +export async function processUnattachedDocsEntries( + entries: DocsIndexEntry[] +): Promise> { + const entriesWithContent = await Promise.all(entries.map(createDocsManifestEntry)); + return Object.fromEntries(entriesWithContent.map((entry) => [entry.id, entry])); +} + +/** + * Processes attached docs entries by adding them to their corresponding component manifests. + * + * Returns the updated components manifest with docs added to each component. + */ +export async function processAttachedDocsEntries( + entries: DocsIndexEntry[], + existingComponents: ComponentsManifestWithDocs | undefined +): Promise { + if (!existingComponents || entries.length === 0) { + return existingComponents; + } + + const entriesWithContent = await Promise.all(entries.map(createDocsManifestEntry)); + + // Create a copy of the components manifest to modify + const updatedComponents: Record = {}; + + for (const [componentId, component] of Object.entries(existingComponents.components)) { + updatedComponents[componentId] = { ...component }; + } + + // Add docs to their corresponding components based on the entry id prefix + for (const docsEntry of entriesWithContent) { + // Extract the component id from the docs entry id (e.g., "example--docs" -> "example") + const componentId = docsEntry.id.split('--')[0]; + + if (updatedComponents[componentId]) { + const component = updatedComponents[componentId]; + if (!component.docs) { + component.docs = {}; + } + component.docs[docsEntry.id] = docsEntry; + } + } + + return { + ...existingComponents, + components: updatedComponents, + }; +} + /** * Generates a manifest of docs entries. This extends the existing manifest system to include docs * entries. + * + * - Unattached MDX entries (with 'unattached-mdx' tag) are added to a separate `docs` manifest. + * - Attached docs entries are added to their corresponding component manifests under a `docs` + * property. */ export const manifests: PresetPropertyFn< 'experimental_manifests', @@ -43,46 +144,36 @@ export const manifests: PresetPropertyFn< return existingManifests; } - const entriesWithContent = await Promise.all( - docsEntries.map(async (entry): Promise => { - const absolutePath = path.join(process.cwd(), entry.importPath); - try { - const content = await fs.readFile(absolutePath, 'utf-8'); - return { - id: entry.id, - name: entry.name, - path: entry.importPath, - title: entry.title, - content, - }; - } catch (err) { - return { - id: entry.id, - name: entry.name, - path: entry.importPath, - title: entry.title, - error: { - name: err instanceof Error ? err.name : 'Error', - message: err instanceof Error ? err.message : String(err), - }, - }; - } - }) - ); + // Split entries into unattached (standalone) and attached (component-related) + const unattachedEntries = docsEntries.filter((entry) => entry.tags?.includes(UNATTACHED_MDX_TAG)); + const attachedEntries = docsEntries.filter((entry) => !entry.tags?.includes(UNATTACHED_MDX_TAG)); - const docsManifests: Record = Object.fromEntries( - entriesWithContent.map((entry) => [entry.id, entry]) - ); + const currentManifests = existingManifests as ManifestsWithDocs; + + // Process both types of entries + const [unattachedDocs, updatedComponents] = await Promise.all([ + processUnattachedDocsEntries(unattachedEntries), + processAttachedDocsEntries(attachedEntries, currentManifests.components), + ]); logger.verbose( - `Docs manifest generation took ${performance.now() - startPerformance}ms for ${docsEntries.length} entries` + `Docs manifest generation took ${performance.now() - startPerformance}ms for ${docsEntries.length} entries (${unattachedEntries.length} unattached, ${attachedEntries.length} attached)` ); - return { - ...existingManifests, - docs: { + const result: ManifestsWithDocs = { ...currentManifests }; + + // Add unattached docs to the docs manifest + if (Object.keys(unattachedDocs).length > 0) { + result.docs = { v: 0, - docs: docsManifests, - } satisfies DocsManifest, - }; + docs: unattachedDocs, + } satisfies DocsManifest; + } + + // Update the components manifest with attached docs + if (updatedComponents) { + result.components = updatedComponents; + } + + return result; }; From dda4c93cd6be78a8487adcd4be3acb9e875464f6 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 23 Dec 2025 09:53:33 +0100 Subject: [PATCH 08/11] correctly handle attached vs unattached docs --- code/addons/docs/src/manifest.test.ts | 40 +++++++++++++++- code/addons/docs/src/manifest.ts | 66 +++++++++++++++------------ 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/code/addons/docs/src/manifest.test.ts b/code/addons/docs/src/manifest.test.ts index 2d836bd48266..1336d49cbbed 100644 --- a/code/addons/docs/src/manifest.test.ts +++ b/code/addons/docs/src/manifest.test.ts @@ -82,6 +82,42 @@ describe('experimental_manifests', () => { expect(result).toEqual(existingManifests); }); + it('should drop docs entries without attached-mdx or unattached-mdx tags', async () => { + const existingManifests = { + components: { + v: 0, + components: { + example: { + id: 'example', + path: './Example.stories.tsx', + name: 'Example', + stories: [], + jsDocTags: {}, + }, + }, + }, + }; + const manifestEntries: IndexEntry[] = [ + { + id: 'example--docs', + name: 'docs', + title: 'Example', + type: 'docs', + importPath: './Example.mdx', + tags: ['manifest', 'autodocs'], // No attached-mdx or unattached-mdx tag + storiesImports: ['./Example.stories.tsx'], + } satisfies DocsIndexEntry, + ]; + + const result = (await manifests(existingManifests, { + manifestEntries, + } as any)) as ManifestResult; + + // Should return existing manifests unchanged since docs entry was dropped + expect(result).toEqual(existingManifests); + expect(result.components?.components.example.docs).toBeUndefined(); + }); + it('should add attached docs entries to component manifests', async () => { const existingManifests = { components: { @@ -104,7 +140,7 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest'], + tags: ['manifest', 'attached-mdx'], storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, ]; @@ -178,7 +214,7 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest'], + tags: ['manifest', 'attached-mdx'], storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, { diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts index 88358f5229b9..d913ac4eb687 100644 --- a/code/addons/docs/src/manifest.ts +++ b/code/addons/docs/src/manifest.ts @@ -1,6 +1,7 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; +import { groupBy } from 'storybook/internal/common'; import { logger } from 'storybook/internal/node-logger'; import type { ComponentManifest, @@ -12,6 +13,7 @@ import type { StorybookConfigRaw, } from 'storybook/internal/types'; +const ATTACHED_MDX_TAG = 'attached-mdx'; const UNATTACHED_MDX_TAG = 'unattached-mdx'; export interface DocsManifestEntry { @@ -69,22 +71,25 @@ export async function createDocsManifestEntry(entry: DocsIndexEntry): Promise> { + if (entries.length === 0) { + return {}; + } const entriesWithContent = await Promise.all(entries.map(createDocsManifestEntry)); return Object.fromEntries(entriesWithContent.map((entry) => [entry.id, entry])); } /** - * Processes attached docs entries by adding them to their corresponding component manifests. + * Extracts attached docs entries by adding them to their corresponding component manifests. * * Returns the updated components manifest with docs added to each component. */ -export async function processAttachedDocsEntries( +export async function extractAttachedDocsEntries( entries: DocsIndexEntry[], existingComponents: ComponentsManifestWithDocs | undefined ): Promise { @@ -94,20 +99,12 @@ export async function processAttachedDocsEntries( const entriesWithContent = await Promise.all(entries.map(createDocsManifestEntry)); - // Create a copy of the components manifest to modify - const updatedComponents: Record = {}; - - for (const [componentId, component] of Object.entries(existingComponents.components)) { - updatedComponents[componentId] = { ...component }; - } - // Add docs to their corresponding components based on the entry id prefix for (const docsEntry of entriesWithContent) { - // Extract the component id from the docs entry id (e.g., "example--docs" -> "example") const componentId = docsEntry.id.split('--')[0]; - if (updatedComponents[componentId]) { - const component = updatedComponents[componentId]; + const component = existingComponents.components[componentId]; + if (component) { if (!component.docs) { component.docs = {}; } @@ -115,10 +112,7 @@ export async function processAttachedDocsEntries( } } - return { - ...existingComponents, - components: updatedComponents, - }; + return existingComponents; } /** @@ -126,8 +120,9 @@ export async function processAttachedDocsEntries( * entries. * * - Unattached MDX entries (with 'unattached-mdx' tag) are added to a separate `docs` manifest. - * - Attached docs entries are added to their corresponding component manifests under a `docs` - * property. + * - Attached MDX entries (with 'attached-mdx' tag) are added to their corresponding component + * manifests under a `docs` property. + * - Docs entries without either tag are ignored. */ export const manifests: PresetPropertyFn< 'experimental_manifests', @@ -144,30 +139,41 @@ export const manifests: PresetPropertyFn< return existingManifests; } - // Split entries into unattached (standalone) and attached (component-related) - const unattachedEntries = docsEntries.filter((entry) => entry.tags?.includes(UNATTACHED_MDX_TAG)); - const attachedEntries = docsEntries.filter((entry) => !entry.tags?.includes(UNATTACHED_MDX_TAG)); + const { attachedEntries = [], unattachedEntries = [] } = groupBy(docsEntries, (entry) => { + switch (true) { + case entry.tags?.includes(UNATTACHED_MDX_TAG): + return 'unattachedEntries'; + case entry.tags?.includes(ATTACHED_MDX_TAG): + return 'attachedEntries'; + default: + return 'ignored'; + } + }); - const currentManifests = existingManifests as ManifestsWithDocs; + if (unattachedEntries.length === 0 && attachedEntries.length === 0) { + return existingManifests; + } + + const existingManifestsWithDocs = existingManifests as ManifestsWithDocs; - // Process both types of entries const [unattachedDocs, updatedComponents] = await Promise.all([ - processUnattachedDocsEntries(unattachedEntries), - processAttachedDocsEntries(attachedEntries, currentManifests.components), + extractUnattachedDocsEntries(unattachedEntries), + extractAttachedDocsEntries(attachedEntries, existingManifestsWithDocs.components), ]); + const processedCount = unattachedEntries.length + attachedEntries.length; logger.verbose( - `Docs manifest generation took ${performance.now() - startPerformance}ms for ${docsEntries.length} entries (${unattachedEntries.length} unattached, ${attachedEntries.length} attached)` + `Docs manifest generation took ${performance.now() - startPerformance}ms for ${processedCount} entries (${unattachedEntries.length} unattached, ${attachedEntries.length} attached)` ); - const result: ManifestsWithDocs = { ...currentManifests }; + const result = { ...existingManifestsWithDocs }; // Add unattached docs to the docs manifest if (Object.keys(unattachedDocs).length > 0) { result.docs = { v: 0, docs: unattachedDocs, - } satisfies DocsManifest; + }; } // Update the components manifest with attached docs From 532ac710b414567c5cf0e9f4c7368e8e765f6f3c Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Mon, 29 Dec 2025 09:55:11 +0100 Subject: [PATCH 09/11] add component manifest entries for attached mdx --- .../src/componentManifest/generator.test.ts | 106 ++++++++++++++++++ .../react/src/componentManifest/generator.ts | 23 +++- 2 files changed, 125 insertions(+), 4 deletions(-) diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index f33b2b88862b..0759a56ff156 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -457,3 +457,109 @@ test('unknown expressions', async () => { } `); }); + +test('should create component manifest when only attached-mdx docs have manifest tag', async () => { + // This test verifies that the React renderer creates a component manifest entry + // when only an attached-mdx docs entry has the 'manifest' tag (and no story entries do). + // Note: The `docs` property of the component manifest is added by addon-docs, not by this generator, + // so it is not part of this test's snapshot. + vol.fromJSON( + { + ['./package.json']: JSON.stringify({ name: 'some-package' }), + ['./src/stories/Button.stories.ts']: dedent` + import type { Meta, StoryObj } from '@storybook/react'; + import { fn } from 'storybook/test'; + import { Button } from './Button'; + + const meta = { + title: 'Example/Button', + component: Button, + args: { onClick: fn() }, + } satisfies Meta; + export default meta; + type Story = StoryObj; + + export const Primary: Story = { args: { primary: true, label: 'Button', tags: ['!manifest'] } }; + `, + ['./src/stories/Button.tsx']: dedent` + import React from 'react'; + export interface ButtonProps { + /** Description of primary */ + primary?: boolean; + label: string; + } + + /** Primary UI component for user interaction */ + export const Button = ({ + primary = false, + label, + }: ButtonProps) => { + return ; + }; + `, + }, + '/app' + ); + + // Only docs entry has manifest tag, story does not + const manifestEntries = [ + { + type: 'docs', + id: 'example-button--docs', + name: 'Docs', + title: 'Example/Button', + importPath: './src/stories/Button.mdx', + tags: ['dev', 'test', 'manifest', 'attached-mdx'], + storiesImports: ['./src/stories/Button.stories.ts'], + }, + ]; + + expect(await manifests(undefined, { manifestEntries } as any)).toMatchInlineSnapshot(` + { + "components": { + "components": { + "example-button": { + "description": "Primary UI component for user interaction", + "error": undefined, + "id": "example-button", + "import": "import { Button } from "some-package";", + "jsDocTags": {}, + "name": "Button", + "path": "./src/stories/Button.stories.ts", + "reactDocgen": { + "actualName": "Button", + "definedInFile": "./src/stories/Button.tsx", + "description": "Primary UI component for user interaction", + "displayName": "Button", + "exportName": "Button", + "methods": [], + "props": { + "label": { + "description": "", + "required": true, + "tsType": { + "name": "string", + }, + }, + "primary": { + "defaultValue": { + "computed": false, + "value": "false", + }, + "description": "Description of primary", + "required": false, + "tsType": { + "name": "boolean", + }, + }, + }, + }, + "stories": [], + "summary": undefined, + }, + }, + "v": 0, + }, + } + `); +}); diff --git a/code/renderers/react/src/componentManifest/generator.ts b/code/renderers/react/src/componentManifest/generator.ts index 74cb3079bd9a..12f70f6a8036 100644 --- a/code/renderers/react/src/componentManifest/generator.ts +++ b/code/renderers/react/src/componentManifest/generator.ts @@ -1,7 +1,7 @@ import { recast } from 'storybook/internal/babel'; import { extractDescription, loadCsf } from 'storybook/internal/csf-tools'; import { logger } from 'storybook/internal/node-logger'; -import type { IndexEntry } from 'storybook/internal/types'; +import type { DocsIndexEntry, IndexEntry } from 'storybook/internal/types'; import { type ComponentManifest, type PresetPropertyFn, @@ -17,6 +17,8 @@ import { extractJSDocInfo } from './jsdocTags'; import { type DocObj } from './reactDocgen'; import { cachedFindUp, cachedReadFileSync, invalidateCache, invariant } from './utils'; +const ATTACHED_MDX_TAG = 'attached-mdx'; + interface ReactComponentManifest extends ComponentManifest { reactDocgen?: DocObj; } @@ -105,13 +107,26 @@ export const manifests: PresetPropertyFn< const startPerformance = performance.now(); const entriesByUniqueComponent = uniqBy( - manifestEntries.filter((entry) => entry.type === 'story' && entry.subtype === 'story'), + manifestEntries.filter( + (entry) => + (entry.type === 'story' && entry.subtype === 'story') || + // addon-docs will add docs entries to these manifest entries afterwards + // Docs entries have importPath pointing to MDX file, but storiesImports[0] points to the story file + (entry.type === 'docs' && + entry.tags?.includes(ATTACHED_MDX_TAG) && + entry.storiesImports.length > 0) + ), (entry) => entry.id.split('--')[0] ); const components = entriesByUniqueComponent .map((entry): ReactComponentManifest | undefined => { - const absoluteImportPath = path.join(process.cwd(), entry.importPath); + const storyFilePath = + entry.type === 'story' + ? entry.importPath + : // For attached docs entries, storiesImports[0] points to the stories file being attached to + (entry as DocsIndexEntry).storiesImports[0]; + const absoluteImportPath = path.join(process.cwd(), storyFilePath); const storyFile = cachedReadFileSync(absoluteImportPath, 'utf-8') as string; const csf = loadCsf(storyFile, { makeTitle: (title) => title ?? 'No title' }).parse(); @@ -137,7 +152,7 @@ export const manifests: PresetPropertyFn< const base = { id, name: componentName ?? title, - path: entry.importPath, + path: storyFilePath, stories, import: imports, jsDocTags: {}, From 0a404c6f133b0707b64b55d7547c0e6bace9c296 Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 6 Jan 2026 11:37:17 +0100 Subject: [PATCH 10/11] replace hardcoded string literal tags with common Tag.X enum usage --- .../addons/docs/src/blocks/blocks/Stories.tsx | 5 +- .../blocks/blocks/usePrimaryStory.test.tsx | 7 ++- .../docs/src/blocks/blocks/usePrimaryStory.ts | 3 +- code/addons/docs/src/manifest.test.ts | 21 +++---- code/addons/docs/src/manifest.ts | 8 +-- code/addons/vitest/src/manager.tsx | 4 +- .../vitest/src/node/test-manager.test.ts | 14 ++--- code/addons/vitest/src/node/vitest-manager.ts | 3 +- code/addons/vitest/src/vitest-plugin/index.ts | 3 +- code/core/src/core-server/index.ts | 2 + .../src/core-server/presets/common-manager.ts | 4 +- .../utils/StoryIndexGenerator.test.ts | 9 +-- .../core-server/utils/StoryIndexGenerator.ts | 20 +++---- .../utils/__tests__/index-extraction.test.ts | 7 ++- .../utils/manifests/manifests.test.ts | 5 +- .../core-server/utils/manifests/manifests.ts | 3 +- .../core-server/utils/summarizeIndex.test.ts | 57 ++++++++++--------- .../src/core-server/utils/summarizeIndex.ts | 10 ++-- code/core/src/csf-tools/CsfFile.ts | 10 ++-- .../vitest-plugin/transformer.test.ts | 11 ++-- code/core/src/csf/csf-factories.ts | 8 +-- code/core/src/manager-api/index.mock.ts | 1 + code/core/src/manager-api/index.ts | 2 + code/core/src/manager-api/lib/stories.ts | 4 +- .../manager/components/sidebar/TagsFilter.tsx | 16 ++---- code/core/src/manager/globals/exports.ts | 1 + code/core/src/preview-api/index.ts | 2 + .../preview-web/PreviewWeb.mockdata.ts | 9 +-- .../preview-web/PreviewWithSelection.tsx | 9 +-- .../preview-web/render/CsfDocsRender.test.ts | 3 +- .../store/csf/portable-stories.test.ts | 7 ++- .../modules/store/csf/prepareStory.test.ts | 7 ++- .../modules/store/csf/prepareStory.ts | 7 ++- .../shared/checklist-store/checklistData.tsx | 6 +- code/core/src/shared/constants/tags.ts | 25 ++++++++ .../automigrate/fixes/remove-docs-autodocs.ts | 5 +- .../react/src/componentManifest/fixtures.ts | 16 +++--- .../src/componentManifest/generator.test.ts | 8 ++- .../react/src/componentManifest/generator.ts | 5 +- code/renderers/react/src/entry-preview.tsx | 3 +- 40 files changed, 193 insertions(+), 157 deletions(-) create mode 100644 code/core/src/shared/constants/tags.ts diff --git a/code/addons/docs/src/blocks/blocks/Stories.tsx b/code/addons/docs/src/blocks/blocks/Stories.tsx index 495183304a08..0a23ecd66da1 100644 --- a/code/addons/docs/src/blocks/blocks/Stories.tsx +++ b/code/addons/docs/src/blocks/blocks/Stories.tsx @@ -1,6 +1,7 @@ import type { FC, ReactElement } from 'react'; import React, { useContext } from 'react'; +import { Tag } from 'storybook/internal/preview-api'; import { styled } from 'storybook/theming'; import { DocsContext } from './DocsContext'; @@ -43,11 +44,11 @@ export const Stories: FC = ({ title = 'Stories', includePrimary = // The new behavior here is that if NONE of the stories in the autodocs page are tagged // with 'autodocs', we show all stories. If ANY of the stories have autodocs then we use // the new behavior. - const hasAutodocsTaggedStory = stories.some((story) => story.tags?.includes('autodocs')); + const hasAutodocsTaggedStory = stories.some((story) => story.tags?.includes(Tag.AUTODOCS)); if (hasAutodocsTaggedStory) { // Don't show stories where mount is used in docs. // As the play function is not running in docs, and when mount is used, the mounting is happening in play itself. - stories = stories.filter((story) => story.tags?.includes('autodocs') && !story.usesMount); + stories = stories.filter((story) => story.tags?.includes(Tag.AUTODOCS) && !story.usesMount); } if (!includePrimary) { diff --git a/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx b/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx index aa92719b802a..b6bb7c037053 100644 --- a/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx +++ b/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx @@ -10,11 +10,12 @@ import type { PreparedStory } from 'storybook/internal/types'; import type { DocsContextProps } from './DocsContext'; import { DocsContext } from './DocsContext'; import { usePrimaryStory } from './usePrimaryStory'; +import { Tag } from 'storybook/internal/core-server'; const stories: Record> = { - story1: { name: 'Story One', tags: ['!autodocs'] }, - story2: { name: 'Story Two', tags: ['autodocs'] }, - story3: { name: 'Story Three', tags: ['autodocs'] }, + story1: { name: 'Story One', tags: [`!${Tag.AUTODOCS}`] }, + story2: { name: 'Story Two', tags: [Tag.AUTODOCS] }, + story3: { name: 'Story Three', tags: [Tag.AUTODOCS] }, story4: { name: 'Story Four', tags: [] }, }; diff --git a/code/addons/docs/src/blocks/blocks/usePrimaryStory.ts b/code/addons/docs/src/blocks/blocks/usePrimaryStory.ts index fd177287023e..75d085a69b17 100644 --- a/code/addons/docs/src/blocks/blocks/usePrimaryStory.ts +++ b/code/addons/docs/src/blocks/blocks/usePrimaryStory.ts @@ -1,5 +1,6 @@ import { useContext } from 'react'; +import { Tag } from 'storybook/internal/preview-api'; import type { PreparedStory } from 'storybook/internal/types'; import { DocsContext } from './DocsContext'; @@ -11,5 +12,5 @@ import { DocsContext } from './DocsContext'; export const usePrimaryStory = (): PreparedStory | undefined => { const context = useContext(DocsContext); const stories = context.componentStories(); - return stories.find((story) => story.tags.includes('autodocs')); + return stories.find((story) => story.tags.includes(Tag.AUTODOCS)); }; diff --git a/code/addons/docs/src/manifest.test.ts b/code/addons/docs/src/manifest.test.ts index 1336d49cbbed..2f020dc64382 100644 --- a/code/addons/docs/src/manifest.test.ts +++ b/code/addons/docs/src/manifest.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { Tag } from 'storybook/internal/core-server'; import type { DocsIndexEntry, IndexEntry } from 'storybook/internal/types'; import { vol } from 'memfs'; @@ -71,7 +72,7 @@ describe('experimental_manifests', () => { type: 'story', subtype: 'story', importPath: './Example.stories.tsx', - tags: ['manifest'], + tags: [Tag.MANIFEST], }, ]; @@ -104,7 +105,7 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest', 'autodocs'], // No attached-mdx or unattached-mdx tag + tags: [Tag.MANIFEST, Tag.AUTODOCS], // No attached-mdx or unattached-mdx tag storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, ]; @@ -140,7 +141,7 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest', 'attached-mdx'], + tags: [Tag.MANIFEST, Tag.ATTACHED_MDX], storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, ]; @@ -170,7 +171,7 @@ describe('experimental_manifests', () => { title: 'Standalone', type: 'docs', importPath: './Standalone.mdx', - tags: ['manifest', 'unattached-mdx'], + tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX], storiesImports: [], } satisfies DocsIndexEntry, ]; @@ -214,7 +215,7 @@ describe('experimental_manifests', () => { title: 'Example', type: 'docs', importPath: './Example.mdx', - tags: ['manifest', 'attached-mdx'], + tags: [Tag.MANIFEST, Tag.ATTACHED_MDX], storiesImports: ['./Example.stories.tsx'], } satisfies DocsIndexEntry, { @@ -223,7 +224,7 @@ describe('experimental_manifests', () => { title: 'Standalone', type: 'docs', importPath: './Standalone.mdx', - tags: ['manifest', 'unattached-mdx'], + tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX], storiesImports: [], } satisfies DocsIndexEntry, ]; @@ -274,7 +275,7 @@ describe('experimental_manifests', () => { title: 'Standalone', type: 'docs', importPath: './Standalone.mdx', - tags: ['manifest', 'unattached-mdx'], + tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX], storiesImports: [], } satisfies DocsIndexEntry, ]; @@ -297,7 +298,7 @@ describe('experimental_manifests', () => { title: 'Missing', type: 'docs', importPath: './NonExistent.mdx', - tags: ['manifest', 'unattached-mdx'], + tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX], storiesImports: [], } satisfies DocsIndexEntry, ]; @@ -326,7 +327,7 @@ describe('experimental_manifests', () => { title: 'Standalone', type: 'docs', importPath: './Standalone.mdx', - tags: ['manifest', 'unattached-mdx'], + tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX], storiesImports: [], } satisfies DocsIndexEntry, { @@ -335,7 +336,7 @@ describe('experimental_manifests', () => { title: 'Missing', type: 'docs', importPath: './NonExistent.mdx', - tags: ['manifest', 'unattached-mdx'], + tags: [Tag.MANIFEST, Tag.UNATTACHED_MDX], storiesImports: [], } satisfies DocsIndexEntry, ]; diff --git a/code/addons/docs/src/manifest.ts b/code/addons/docs/src/manifest.ts index d913ac4eb687..59e4efa4af5b 100644 --- a/code/addons/docs/src/manifest.ts +++ b/code/addons/docs/src/manifest.ts @@ -2,6 +2,7 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { groupBy } from 'storybook/internal/common'; +import { Tag } from 'storybook/internal/core-server'; import { logger } from 'storybook/internal/node-logger'; import type { ComponentManifest, @@ -13,9 +14,6 @@ import type { StorybookConfigRaw, } from 'storybook/internal/types'; -const ATTACHED_MDX_TAG = 'attached-mdx'; -const UNATTACHED_MDX_TAG = 'unattached-mdx'; - export interface DocsManifestEntry { id: string; name: string; @@ -141,9 +139,9 @@ export const manifests: PresetPropertyFn< const { attachedEntries = [], unattachedEntries = [] } = groupBy(docsEntries, (entry) => { switch (true) { - case entry.tags?.includes(UNATTACHED_MDX_TAG): + case entry.tags?.includes(Tag.UNATTACHED_MDX): return 'unattachedEntries'; - case entry.tags?.includes(ATTACHED_MDX_TAG): + case entry.tags?.includes(Tag.ATTACHED_MDX): return 'attachedEntries'; default: return 'ignored'; diff --git a/code/addons/vitest/src/manager.tsx b/code/addons/vitest/src/manager.tsx index f12c41fe95d7..84753db1a9aa 100644 --- a/code/addons/vitest/src/manager.tsx +++ b/code/addons/vitest/src/manager.tsx @@ -8,7 +8,7 @@ import { store, testProviderStore, } from '#manager-store'; -import { addons } from 'storybook/manager-api'; +import { addons, Tag } from 'storybook/manager-api'; import { GlobalErrorContext, GlobalErrorModal } from './components/GlobalErrorModal'; import { SidebarContextMenu } from './components/SidebarContextMenu'; @@ -95,7 +95,7 @@ addons.register(ADDON_ID, (api) => { if (context.type === 'docs') { return null; } - if (context.type === 'story' && !context.tags.includes('test')) { + if (context.type === 'story' && !context.tags.includes(Tag.TEST)) { return null; } return ; diff --git a/code/addons/vitest/src/node/test-manager.test.ts b/code/addons/vitest/src/node/test-manager.test.ts index 0845f8dc8bd2..3a5668e645f3 100644 --- a/code/addons/vitest/src/node/test-manager.test.ts +++ b/code/addons/vitest/src/node/test-manager.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { Channel, type ChannelTransport } from 'storybook/internal/channels'; -import { experimental_MockUniversalStore } from 'storybook/internal/core-server'; +import { Tag, experimental_MockUniversalStore } from 'storybook/internal/core-server'; import type { Options, StatusStoreByTypeId, @@ -115,7 +115,7 @@ global.fetch = vi.fn().mockResolvedValue({ name: 'One', title: 'story/one', importPath: 'path/to/file', - tags: ['test'], + tags: [Tag.TEST], }, 'another--one': { type: 'story', @@ -124,7 +124,7 @@ global.fetch = vi.fn().mockResolvedValue({ name: 'One', title: 'another/one', importPath: 'path/to/another/file', - tags: ['test'], + tags: [Tag.TEST], }, 'parent--story': { type: 'story', @@ -133,17 +133,17 @@ global.fetch = vi.fn().mockResolvedValue({ name: 'Parent story', title: 'parent/story', importPath: 'path/to/parent/file', - tags: ['test'], + tags: [Tag.TEST], }, 'parent--story:test': { type: 'story', - subtype: 'test', + subtype: Tag.TEST, id: 'parent--story:test', name: 'Test name', title: 'parent/story', parent: 'parent--story', importPath: 'path/to/parent/file', - tags: ['test', 'test-fn'], + tags: [Tag.TEST, Tag.TEST_FN], }, }, } as StoryIndex) @@ -278,7 +278,7 @@ describe('TestManager', () => { expect(createVitest).toHaveBeenCalledTimes(1); expect(createVitest).toHaveBeenCalledWith( - 'test', + Tag.TEST, expect.objectContaining({ coverage: expect.objectContaining({ enabled: true }), }) diff --git a/code/addons/vitest/src/node/vitest-manager.ts b/code/addons/vitest/src/node/vitest-manager.ts index c6c53908b01a..8cf3e0f15e7e 100644 --- a/code/addons/vitest/src/node/vitest-manager.ts +++ b/code/addons/vitest/src/node/vitest-manager.ts @@ -9,6 +9,7 @@ import type { } from 'vitest/node'; import { getProjectRoot, resolvePathInStorybookCache } from 'storybook/internal/common'; +import { Tag } from 'storybook/internal/core-server'; import type { StoryId, StoryIndex, StoryIndexEntry } from 'storybook/internal/types'; import * as find from 'empathic/find'; @@ -221,7 +222,7 @@ export class VitestManager { for (const testSpecification of testSpecifications) { const { env = {} } = testSpecification.project.config; - const include = env.__VITEST_INCLUDE_TAGS__?.split(',').filter(Boolean) ?? ['test']; + const include = env.__VITEST_INCLUDE_TAGS__?.split(',').filter(Boolean) ?? [Tag.TEST]; const exclude = env.__VITEST_EXCLUDE_TAGS__?.split(',').filter(Boolean) ?? []; const skip = env.__VITEST_SKIP_TAGS__?.split(',').filter(Boolean) ?? []; diff --git a/code/addons/vitest/src/vitest-plugin/index.ts b/code/addons/vitest/src/vitest-plugin/index.ts index 973eeb0d8cac..70c82a25d4af 100644 --- a/code/addons/vitest/src/vitest-plugin/index.ts +++ b/code/addons/vitest/src/vitest-plugin/index.ts @@ -14,6 +14,7 @@ import { } from 'storybook/internal/common'; import { StoryIndexGenerator, + Tag, experimental_loadStorybook, mapStaticDir, } from 'storybook/internal/core-server'; @@ -105,7 +106,7 @@ export const storybookTest = async (options?: UserOptions): Promise => ? resolve(WORKING_DIR, options.configDir) : defaultOptions.configDir, tags: { - include: options?.tags?.include ?? ['test'], + include: options?.tags?.include ?? [Tag.TEST], exclude: options?.tags?.exclude ?? [], skip: options?.tags?.skip ?? [], }, diff --git a/code/core/src/core-server/index.ts b/code/core/src/core-server/index.ts index a36bf54ceca0..9e4e76ea230a 100644 --- a/code/core/src/core-server/index.ts +++ b/code/core/src/core-server/index.ts @@ -12,6 +12,8 @@ export { StoryIndexGenerator } from './utils/StoryIndexGenerator'; export { loadStorybook as experimental_loadStorybook } from './load'; +export { Tag } from '../shared/constants/tags'; + export { UniversalStore as experimental_UniversalStore } from '../shared/universal-store'; export { MockUniversalStore as experimental_MockUniversalStore } from '../shared/universal-store/mock'; export { diff --git a/code/core/src/core-server/presets/common-manager.ts b/code/core/src/core-server/presets/common-manager.ts index 563443222ac3..bd4c358e837b 100644 --- a/code/core/src/core-server/presets/common-manager.ts +++ b/code/core/src/core-server/presets/common-manager.ts @@ -1,7 +1,7 @@ /* these imports are in the exact order in which the panels need to be registered */ import { global } from '@storybook/global'; -import { addons } from 'storybook/manager-api'; +import { addons, Tag } from 'storybook/manager-api'; // THE ORDER OF THESE IMPORTS MATTERS! IT DEFINES THE ORDER OF PANELS AND TOOLS! import controlsManager from '../../controls/manager'; @@ -33,7 +33,7 @@ const tagFiltersManager = addons.register(TAG_FILTERS, (api) => { const tags = item.tags ?? []; return ( // we can filter out the primary story, but we still want to show autodocs - (tags.includes('dev') || item.type === 'docs') && + (tags.includes(Tag.DEV) || item.type === 'docs') && tags.filter((tag) => staticExcludeTags[tag]).length === 0 ); }); diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts index f9359d21715c..857f06b12542 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.test.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.test.ts @@ -8,6 +8,7 @@ import { getStorySortParameter, readCsf } from 'storybook/internal/csf-tools'; import { logger, once } from 'storybook/internal/node-logger'; import type { NormalizedStoriesSpecifier, StoryIndexEntry } from 'storybook/internal/types'; +import { Tag } from '../../shared/constants/tags'; import { csfIndexer } from '../presets/common-preset'; import type { StoryIndexGeneratorOptions } from './StoryIndexGenerator'; import { StoryIndexGenerator } from './StoryIndexGenerator'; @@ -1021,7 +1022,7 @@ describe('StoryIndexGenerator', () => { ); const generator = new StoryIndexGenerator([specifier], autodocsOptions); - generator.getProjectTags = () => ['dev', 'test', 'autodocs']; + generator.getProjectTags = () => [Tag.DEV, Tag.TEST, Tag.AUTODOCS]; await generator.initialize(); expect(Object.keys((await generator.getIndex()).entries)).toMatchInlineSnapshot(` @@ -1065,12 +1066,12 @@ describe('StoryIndexGenerator', () => { ); const generator = new StoryIndexGenerator([specifier], autodocsOptions); - generator.getProjectTags = () => ['dev', 'test', 'autodocs']; + generator.getProjectTags = () => [Tag.DEV, Tag.TEST, Tag.AUTODOCS]; await generator.initialize(); const index = await generator.getIndex(); expect(index.entries['first-nested-deeply-f--docs'].tags).toEqual( - expect.arrayContaining(['autodocs']) + expect.arrayContaining([Tag.AUTODOCS]) ); }); @@ -1327,7 +1328,7 @@ describe('StoryIndexGenerator', () => { ); const generator = new StoryIndexGenerator([csfSpecifier, docsSpecifier], autodocsOptions); - generator.getProjectTags = () => ['dev', 'test', 'autodocs']; + generator.getProjectTags = () => [Tag.DEV, Tag.TEST, Tag.AUTODOCS]; await generator.initialize(); const { storyIndex } = await generator.getIndexAndStats(); diff --git a/code/core/src/core-server/utils/StoryIndexGenerator.ts b/code/core/src/core-server/utils/StoryIndexGenerator.ts index b71367d03742..f1578cdbfa44 100644 --- a/code/core/src/core-server/utils/StoryIndexGenerator.ts +++ b/code/core/src/core-server/utils/StoryIndexGenerator.ts @@ -19,7 +19,6 @@ import type { StoryIndexEntry, StoryIndexInput, StorybookConfigRaw, - Tag, } from 'storybook/internal/types'; import * as find from 'empathic/find'; @@ -33,6 +32,7 @@ import * as TsconfigPaths from 'tsconfig-paths'; import { resolveImport, supportedExtensions } from '../../common'; import { userOrAutoTitleFromSpecifier } from '../../preview-api/modules/store/autoTitle'; import { sortStoriesV7 } from '../../preview-api/modules/store/sortStories'; +import { Tag } from '../../shared/constants/tags'; import { IndexingError, MultipleIndexingError } from './IndexingError'; import { autoName } from './autoName'; import { type IndexStatsSummary, addStats } from './summarizeStats'; @@ -64,15 +64,9 @@ export type StoryIndexGeneratorOptions = { build?: StorybookConfigRaw['build']; }; -export const AUTODOCS_TAG = 'autodocs'; -export const ATTACHED_MDX_TAG = 'attached-mdx'; -export const UNATTACHED_MDX_TAG = 'unattached-mdx'; -export const PLAY_FN_TAG = 'play-fn'; -export const TEST_FN_TAG = 'test-fn'; - /** Was this docs entry generated by a .mdx file? (see discussion below) */ export function isMdxEntry({ tags }: DocsIndexEntry) { - return tags?.includes(UNATTACHED_MDX_TAG) || tags?.includes(ATTACHED_MDX_TAG); + return tags?.includes(Tag.UNATTACHED_MDX) || tags?.includes(Tag.ATTACHED_MDX); } const makeAbsolute = (otherImport: Path, normalizedPath: Path, workingDir: Path) => @@ -486,7 +480,7 @@ export class StoryIndexGenerator { // We need a docs entry attached to the CSF file if either: // a) autodocs is globally enabled // b) we have autodocs enabled for this file - const hasAutodocsTag = storyEntries.some((entry) => entry.tags.includes(AUTODOCS_TAG)); + const hasAutodocsTag = storyEntries.some((entry) => entry.tags.includes(Tag.AUTODOCS)); const createDocEntry = hasAutodocsTag && !!this.options.docs; if (createDocEntry && this.options.build?.test?.disableAutoDocs !== true) { @@ -611,7 +605,7 @@ export class StoryIndexGenerator { ...projectTags, ...(csfEntry?.tags ?? []), ...(result.metaTags ?? []), - csfEntry ? 'attached-mdx' : 'unattached-mdx' + csfEntry ? Tag.ATTACHED_MDX : Tag.UNATTACHED_MDX ); const docsEntry: DocsCacheEntry = { @@ -687,9 +681,9 @@ export class StoryIndexGenerator { } // If you link a file to a tagged CSF file, you have probably made a mistake - if (worseEntry.tags?.includes(AUTODOCS_TAG) && !projectTags?.includes(AUTODOCS_TAG)) { + if (worseEntry.tags?.includes(Tag.AUTODOCS) && !projectTags?.includes(Tag.AUTODOCS)) { throw new IndexingError( - `You created a component docs page for '${worseEntry.title}', but also tagged the CSF file with '${AUTODOCS_TAG}'. This is probably a mistake.`, + `You created a component docs page for '${worseEntry.title}', but also tagged the CSF file with '${Tag.AUTODOCS}'. This is probably a mistake.`, [betterEntry.importPath, worseEntry.importPath] ); } @@ -875,7 +869,7 @@ export class StoryIndexGenerator { getProjectTags(previewCode?: string) { let projectTags = [] as Tag[]; - const defaultTags = ['dev', 'test', 'manifest']; + const defaultTags = [Tag.DEV, Tag.TEST, Tag.MANIFEST]; if (previewCode) { try { const projectAnnotations = loadConfig(previewCode).parse(); diff --git a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts index ab582f0bb3ab..341de83a4ae5 100644 --- a/code/core/src/core-server/utils/__tests__/index-extraction.test.ts +++ b/code/core/src/core-server/utils/__tests__/index-extraction.test.ts @@ -5,8 +5,9 @@ import { describe, expect, it, vi } from 'vitest'; import { normalizeStoriesEntry } from 'storybook/internal/common'; import type { NormalizedStoriesSpecifier } from 'storybook/internal/types'; +import { Tag } from '../../../shared/constants/tags'; import type { StoryIndexGeneratorOptions } from '../StoryIndexGenerator'; -import { AUTODOCS_TAG, StoryIndexGenerator } from '../StoryIndexGenerator'; +import { StoryIndexGenerator } from '../StoryIndexGenerator'; vi.mock('storybook/internal/node-logger'); @@ -419,7 +420,7 @@ describe('story extraction', () => { }); }); describe('docs entries from story extraction', () => { - it(`adds docs entry when autodocs is "tag" and an entry has the "${AUTODOCS_TAG}" tag`, async () => { + it(`adds docs entry when autodocs is "tag" and an entry has the "${Tag.AUTODOCS}" tag`, async () => { const relativePath = './src/A.stories.js'; const absolutePath = join(options.workingDir, relativePath); const specifier: NormalizedStoriesSpecifier = normalizeStoriesEntry(relativePath, options); @@ -436,7 +437,7 @@ describe('docs entries from story extraction', () => { __id: 'a--story-one', name: 'Story One', title: 'A', - tags: [AUTODOCS_TAG, 'story-tag-from-indexer'], + tags: [Tag.AUTODOCS, 'story-tag-from-indexer'], importPath: fileName, type: 'story', subtype: 'story', diff --git a/code/core/src/core-server/utils/manifests/manifests.test.ts b/code/core/src/core-server/utils/manifests/manifests.test.ts index 41ceddf8a452..8111e48b7802 100644 --- a/code/core/src/core-server/utils/manifests/manifests.test.ts +++ b/code/core/src/core-server/utils/manifests/manifests.test.ts @@ -6,6 +6,7 @@ import type { ComponentsManifest, Manifests, Presets, StoryIndex } from 'storybo import { vol } from 'memfs'; import type { Polka, Request, Response } from 'polka'; +import { Tag } from '../../../shared/constants/tags'; import { registerManifests, writeManifests } from './manifests'; // Mock dependencies @@ -130,7 +131,7 @@ describe('manifests', () => { name: 'Story', title: 'Example', importPath: './Example.stories.tsx', - tags: ['manifest', 'other'], + tags: [Tag.MANIFEST, 'other'], }, 'story-without-manifest': { type: 'story', @@ -147,7 +148,7 @@ describe('manifests', () => { name: 'Docs', title: 'Docs', importPath: './Docs.mdx', - tags: ['manifest'], + tags: [Tag.MANIFEST], storiesImports: [], }, }, diff --git a/code/core/src/core-server/utils/manifests/manifests.ts b/code/core/src/core-server/utils/manifests/manifests.ts index a77907df7a59..386c920270ac 100644 --- a/code/core/src/core-server/utils/manifests/manifests.ts +++ b/code/core/src/core-server/utils/manifests/manifests.ts @@ -7,6 +7,7 @@ import { join } from 'pathe'; import type { Polka } from 'polka'; import invariant from 'tiny-invariant'; +import { Tag } from '../../../shared/constants/tags'; import { renderComponentsManifest } from './render-components-manifest'; async function getManifests(presets: Presets) { @@ -14,7 +15,7 @@ async function getManifests(presets: Presets) { invariant(generator, 'storyIndexGenerator must be configured'); const index = await generator.getIndex(); const manifestEntries = Object.values(index.entries).filter( - (entry) => entry.tags?.includes('manifest') ?? false + (entry) => entry.tags?.includes(Tag.MANIFEST) ?? false ); return ( diff --git a/code/core/src/core-server/utils/summarizeIndex.test.ts b/code/core/src/core-server/utils/summarizeIndex.test.ts index a90e0b27f703..7e86aafb7c27 100644 --- a/code/core/src/core-server/utils/summarizeIndex.test.ts +++ b/code/core/src/core-server/utils/summarizeIndex.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import { Tag } from '../../shared/constants/tags'; import { isPageStory, summarizeIndex } from './summarizeIndex'; describe('isPageStory', () => { @@ -56,7 +57,7 @@ describe('summarizeIndex', () => { name: 'Docs', importPath: './src/stories/Button.stories.ts', type: 'docs', - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], storiesImports: [], }, 'example-button--primary': { @@ -64,7 +65,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Primary', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -73,7 +74,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Secondary', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -82,7 +83,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Large', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -91,7 +92,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Small', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -101,7 +102,7 @@ describe('summarizeIndex', () => { name: 'Docs', importPath: './src/stories/Header.stories.ts', type: 'docs', - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], storiesImports: [], }, 'example-header--logged-in': { @@ -109,7 +110,7 @@ describe('summarizeIndex', () => { title: 'Example/Header', name: 'Logged In', importPath: './src/stories/Header.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -118,7 +119,7 @@ describe('summarizeIndex', () => { title: 'Example/Header', name: 'Logged Out', importPath: './src/stories/Header.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -136,7 +137,7 @@ describe('summarizeIndex', () => { title: 'Example/Page', name: 'Logged In', importPath: './src/stories/Page.stories.ts', - tags: ['play-fn', 'story'], + tags: [Tag.PLAY_FN, 'story'], type: 'story', subtype: 'story', }, @@ -183,7 +184,7 @@ describe('summarizeIndex', () => { name: 'Docs', importPath: './src/stories/Button.stories.ts', type: 'docs', - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], storiesImports: [], }, 'example-button--primary': { @@ -191,7 +192,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Primary', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -200,7 +201,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Warning', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story', 'svelte-csf-v4'], + tags: [Tag.AUTODOCS, 'story', 'svelte-csf-v4'], type: 'story', subtype: 'story', }, @@ -303,7 +304,7 @@ describe('summarizeIndex', () => { name: 'Default', title: 'component-testing/test-fn', importPath: './core/src/component-testing/components/test-fn.stories.tsx', - tags: ['dev', 'test', 'vitest', 'some-tag'], + tags: [Tag.DEV, Tag.TEST, 'vitest', 'some-tag'], }, 'component-testing-test-fn--default:simple': { type: 'story', @@ -312,7 +313,7 @@ describe('summarizeIndex', () => { name: 'simple', title: 'component-testing/test-fn', importPath: './core/src/component-testing/components/test-fn.stories.tsx', - tags: ['dev', 'test', 'vitest', 'some-tag', 'test-fn'], + tags: [Tag.DEV, Tag.TEST, 'vitest', 'some-tag', Tag.TEST_FN], parent: 'component-testing-test-fn--default', }, 'component-testing-test-fn--default:referring-to-function-in-file': { @@ -322,7 +323,7 @@ describe('summarizeIndex', () => { name: 'referring to function in file', title: 'component-testing/test-fn', importPath: './core/src/component-testing/components/test-fn.stories.tsx', - tags: ['dev', 'test', 'vitest', 'some-tag', 'test-fn'], + tags: [Tag.DEV, Tag.TEST, 'vitest', 'some-tag', Tag.TEST_FN], parent: 'component-testing-test-fn--default', }, 'component-testing-test-fn--default:with-overrides': { @@ -332,7 +333,7 @@ describe('summarizeIndex', () => { name: 'with overrides', title: 'component-testing/test-fn', importPath: './core/src/component-testing/components/test-fn.stories.tsx', - tags: ['dev', 'test', 'vitest', 'some-tag', 'test-fn'], + tags: [Tag.DEV, Tag.TEST, 'vitest', 'some-tag', Tag.TEST_FN], parent: 'component-testing-test-fn--default', }, 'component-testing-test-fn--default:with-play-function': { @@ -342,7 +343,7 @@ describe('summarizeIndex', () => { name: 'with play function', title: 'component-testing/test-fn', importPath: './core/src/component-testing/components/test-fn.stories.tsx', - tags: ['dev', 'test', 'vitest', 'some-tag', 'test-fn'], + tags: [Tag.DEV, Tag.TEST, 'vitest', 'some-tag', Tag.TEST_FN], parent: 'component-testing-test-fn--default', }, 'component-testing-test-fn--default-extended': { @@ -352,7 +353,7 @@ describe('summarizeIndex', () => { name: 'Default Extended', title: 'component-testing/test-fn', importPath: './core/src/component-testing/components/test-fn.stories.tsx', - tags: ['dev', 'test', 'vitest', 'some-tag'], + tags: [Tag.DEV, Tag.TEST, 'vitest', 'some-tag'], }, 'component-testing-test-fn--default-extended:should-have-extended-args': { type: 'story', @@ -361,7 +362,7 @@ describe('summarizeIndex', () => { name: 'should have extended args', title: 'component-testing/test-fn', importPath: './core/src/component-testing/components/test-fn.stories.tsx', - tags: ['dev', 'test', 'vitest', 'some-tag', 'test-fn'], + tags: [Tag.DEV, Tag.TEST, 'vitest', 'some-tag', Tag.TEST_FN], parent: 'component-testing-test-fn--default-extended', }, }, @@ -406,7 +407,7 @@ describe('summarizeIndex', () => { title: 'Example/Page', name: 'Logged In', importPath: './src/stories/Page.stories.ts', - tags: ['play-fn', 'story'], + tags: [Tag.PLAY_FN, 'story'], type: 'story', subtype: 'story', }, @@ -416,7 +417,7 @@ describe('summarizeIndex', () => { name: 'Docs', importPath: './template-stories/addons/docs/docspage/autoplay.stories.ts', type: 'docs', - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], storiesImports: [], }, 'addons-docs-docspage-autoplay--no-autoplay': { @@ -424,7 +425,7 @@ describe('summarizeIndex', () => { title: 'addons/docs/docspage/autoplay', name: 'No Autoplay', importPath: './template-stories/addons/docs/docspage/autoplay.stories.ts', - tags: ['play-fn', 'story'], + tags: [Tag.PLAY_FN, 'story'], type: 'story', subtype: 'story', }, @@ -462,7 +463,7 @@ describe('summarizeIndex', () => { name: 'Docs', importPath: './src/stories/Button.stories.ts', type: 'docs', - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], storiesImports: [], }, 'example-button--large': { @@ -470,7 +471,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Large', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -479,7 +480,7 @@ describe('summarizeIndex', () => { title: 'Example/Button', name: 'Small', importPath: './src/stories/Button.stories.ts', - tags: ['autodocs', 'story'], + tags: [Tag.AUTODOCS, 'story'], type: 'story', subtype: 'story', }, @@ -489,7 +490,7 @@ describe('summarizeIndex', () => { name: 'Docs', importPath: './template-stories/lib/preview-api/shortcuts.stories.ts', type: 'docs', - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], storiesImports: [], }, }, @@ -536,7 +537,7 @@ describe('summarizeIndex', () => { importPath: './template-stories/addons/docs/docs2/NoTitle.mdx', storiesImports: [], type: 'docs', - tags: ['docs', 'attached-mdx'], + tags: ['docs', Tag.ATTACHED_MDX], }, 'addons-docs-yabbadabbadooo--docs': { id: 'addons-docs-yabbadabbadooo--docs', @@ -545,7 +546,7 @@ describe('summarizeIndex', () => { importPath: './template-stories/addons/docs/docs2/Title.mdx', storiesImports: [], type: 'docs', - tags: ['docs', 'attached-mdx'], + tags: ['docs', Tag.ATTACHED_MDX], }, }, }) diff --git a/code/core/src/core-server/utils/summarizeIndex.ts b/code/core/src/core-server/utils/summarizeIndex.ts index 5e6520323c78..b55d4e6b24e0 100644 --- a/code/core/src/core-server/utils/summarizeIndex.ts +++ b/code/core/src/core-server/utils/summarizeIndex.ts @@ -1,10 +1,10 @@ import { isExampleStoryId } from 'storybook/internal/telemetry'; import type { IndexEntry, StoryIndex } from 'storybook/internal/types'; -import { AUTODOCS_TAG, PLAY_FN_TAG, TEST_FN_TAG, isMdxEntry } from './StoryIndexGenerator'; +import { Tag } from '../../shared/constants/tags'; +import { isMdxEntry } from './StoryIndexGenerator'; const PAGE_REGEX = /(page|screen)/i; -const SVELTE_CSF_TAG = 'svelte-csf'; export const isPageStory = (storyId: string) => PAGE_REGEX.test(storyId); @@ -63,10 +63,10 @@ export function summarizeIndex(storyIndex: StoryIndex) { if (isPageStory(entry.title)) { pageStoryCount += 1; } - if (entry.tags?.includes(PLAY_FN_TAG)) { + if (entry.tags?.includes(Tag.PLAY_FN)) { playStoryCount += 1; } - if (entry.tags?.includes(TEST_FN_TAG) && entry.parent) { + if (entry.tags?.includes(Tag.TEST_FN) && entry.parent) { testStoryCount += 1; testsPerParentStory.set(entry.parent, (testsPerParentStory.get(entry.parent) ?? 0) + 1); } @@ -78,7 +78,7 @@ export function summarizeIndex(storyIndex: StoryIndex) { } else if (entry.type === 'docs') { if (isMdxEntry(entry)) { mdxCount += 1; - } else if (entry.tags?.includes(AUTODOCS_TAG)) { + } else if (entry.tags?.includes(Tag.AUTODOCS)) { autodocsCount += 1; } } diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts index 44088c6aff2d..34d14229d724 100644 --- a/code/core/src/csf-tools/CsfFile.ts +++ b/code/core/src/csf-tools/CsfFile.ts @@ -19,13 +19,13 @@ import type { IndexInputStats, IndexedCSFFile, StoryAnnotations, - Tag, } from 'storybook/internal/types'; import { dedent } from 'ts-dedent'; import type { PrintResultType } from './PrintResultType'; import { findVarInitialization } from './findVarInitialization'; +import { Tag } from '../shared/constants/tags'; // We add this BabelFile as a temporary workaround to deal with a BabelFileClass "ImportEquals should have a literal source" issue in no link mode with tsup interface BabelFile { @@ -874,7 +874,7 @@ export class CsfFile { const entries = Object.entries(self._stories); self._meta.title = this._options.makeTitle(self._meta?.title as string); if (self._metaAnnotations.play) { - self._meta.tags = [...(self._meta.tags || []), 'play-fn']; + self._meta.tags = [...(self._meta.tags || []), Tag.PLAY_FN]; } self._stories = entries.reduce( (acc, [key, story]) => { @@ -903,7 +903,7 @@ export class CsfFile { acc[key].tags = parseTags(node); } if (play) { - acc[key].tags = [...(acc[key].tags || []), 'play-fn']; + acc[key].tags = [...(acc[key].tags || []), Tag.PLAY_FN]; } const stats = acc[key].__stats; ['play', 'render', 'loaders', 'beforeEach', 'globals', 'tags'].forEach((annotation) => { @@ -1024,10 +1024,10 @@ export class CsfFile { tags: [ ...storyInput.tags, // this tag comes before test tags so users can invert if they like - '!autodocs', + `!${Tag.AUTODOCS}`, ...test.tags, // this tag comes after test tags so users can't change it - 'test-fn', + Tag.TEST_FN, ], __id: test.id, }); diff --git a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts index 840d16f57499..ba39bc10aa96 100644 --- a/code/core/src/csf-tools/vitest-plugin/transformer.test.ts +++ b/code/core/src/csf-tools/vitest-plugin/transformer.test.ts @@ -5,6 +5,7 @@ import { logger } from 'storybook/internal/node-logger'; import { type RawSourceMap, SourceMapConsumer } from 'source-map'; +import { Tag } from '../../shared/constants/tags'; import { vitestTransform as originalTransform } from './transformer'; vi.mock('storybook/internal/common', async (importOriginal) => { @@ -24,7 +25,7 @@ const transform = async ({ code = '', fileName = 'src/components/Button.stories.js', tagsFilter = { - include: ['test'], + include: [Tag.TEST] as string[], exclude: [] as string[], skip: [] as string[], }, @@ -456,7 +457,7 @@ describe('transformer', () => { const result = await transform({ code, - tagsFilter: { include: ['test'], exclude: ['exclude-me'], skip: [] }, + tagsFilter: { include: [Tag.TEST], exclude: ['exclude-me'], skip: [] }, }); expect(result.code).toMatchInlineSnapshot(` @@ -485,7 +486,7 @@ describe('transformer', () => { const result = await transform({ code, - tagsFilter: { include: ['test'], exclude: [], skip: ['skip-me'] }, + tagsFilter: { include: [Tag.TEST], exclude: [], skip: ['skip-me'] }, }); expect(result.code).toMatchInlineSnapshot(` @@ -896,7 +897,7 @@ describe('transformer', () => { const result = await transform({ code, - tagsFilter: { include: ['test'], exclude: ['exclude-me'], skip: [] }, + tagsFilter: { include: [Tag.TEST], exclude: ['exclude-me'], skip: [] }, }); expect(result.code).toMatchInlineSnapshot(` @@ -926,7 +927,7 @@ describe('transformer', () => { const result = await transform({ code, - tagsFilter: { include: ['test'], exclude: [], skip: ['skip-me'] }, + tagsFilter: { include: [Tag.TEST], exclude: [], skip: ['skip-me'] }, }); expect(result.code).toMatchInlineSnapshot(` diff --git a/code/core/src/csf/csf-factories.ts b/code/core/src/csf/csf-factories.ts index d05495db58f7..d62e5ad96498 100644 --- a/code/core/src/csf/csf-factories.ts +++ b/code/core/src/csf/csf-factories.ts @@ -19,6 +19,7 @@ import { normalizeProjectAnnotations, } from '../preview-api/index'; import { mountDestructured } from '../preview-api/modules/preview-web/render/mount-utils'; +import { Tag } from '../shared/constants/tags'; import { getCoreAnnotations } from './core-annotations'; export interface Preview { @@ -61,9 +62,8 @@ export function definePreview extends ProjectAnnotations {} +export interface PreviewAddon + extends ProjectAnnotations {} export function definePreviewAddon( preview: ProjectAnnotations @@ -219,7 +219,7 @@ function defineStory< const test = this.extend({ ...annotations, name, - tags: ['test-fn', '!autodocs', ...(annotations.tags ?? [])], + tags: [Tag.TEST_FN, `!${Tag.AUTODOCS}`, ...(annotations.tags ?? [])], play, }); __children.push(test); diff --git a/code/core/src/manager-api/index.mock.ts b/code/core/src/manager-api/index.mock.ts index 231a30b526ea..2ba8bb7e0212 100644 --- a/code/core/src/manager-api/index.mock.ts +++ b/code/core/src/manager-api/index.mock.ts @@ -1,6 +1,7 @@ import { fn } from 'storybook/test'; export * from './root'; +export { Tag } from '../shared/constants/tags'; export const openInEditor = fn(); diff --git a/code/core/src/manager-api/index.ts b/code/core/src/manager-api/index.ts index a977403fc49a..8b24405ca9af 100644 --- a/code/core/src/manager-api/index.ts +++ b/code/core/src/manager-api/index.ts @@ -22,3 +22,5 @@ export { checklistStore as internal_checklistStore, universalChecklistStore as internal_universalChecklistStore, } from './stores/checklist'; + +export { Tag } from '../shared/constants/tags'; diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index a9413f8119c8..ee32d35ca568 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -19,7 +19,6 @@ import type { StoryId, StoryIndexV2, StoryIndexV3, - Tag, } from 'storybook/internal/types'; import { countBy } from 'es-toolkit/array'; @@ -28,6 +27,7 @@ import memoize from 'memoizerific'; import { dedent } from 'ts-dedent'; import { type API, type State, combineParameters } from '../root'; +import { Tag } from '../../shared/constants/tags'; import intersect from './intersect'; import merge from './merge'; @@ -155,7 +155,7 @@ export const transformStoryIndexV4toV5 = ( (acc, entry) => { acc[entry.id] = { ...entry, - tags: entry.tags ? ['dev', 'test', ...entry.tags] : ['dev'], + tags: entry.tags ? [Tag.DEV, Tag.TEST, ...entry.tags] : [Tag.DEV], }; return acc; diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 5cb200306bd8..8ea1c4fcd7f9 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -4,28 +4,20 @@ import { Badge, Button, PopoverProvider } from 'storybook/internal/components'; import type { API_PreparedIndexEntry, StoryIndex, - Tag, TagsOptions, } from 'storybook/internal/types'; import { BeakerIcon, DocumentIcon, FilterIcon, PlayHollowIcon } from '@storybook/icons'; import type { API } from 'storybook/manager-api'; +import { Tag } from 'storybook/manager-api'; import { color, styled } from 'storybook/theming'; import { type Filter, type FilterFunction, TagsFilterPanel, groupByType } from './TagsFilterPanel'; const TAGS_FILTER = 'tags-filter'; -const BUILT_IN_TAGS = new Set([ - 'dev', - 'test', - 'autodocs', - 'attached-mdx', - 'unattached-mdx', - 'play-fn', - 'test-fn', -]); +const BUILT_IN_TAGS = new Set(Object.values(Tag)); const StyledButton = styled(Button)<{ isHighlighted: boolean }>(({ isHighlighted, theme }) => ({ '&:focus-visible': { @@ -119,8 +111,8 @@ export const TagsFilter = ({ api, indexJson, tagPresets }: TagsFilterProps) => { icon: , ...withCount((entry: API_PreparedIndexEntry, excluded?: boolean) => excluded - ? entry.type !== 'story' || !entry.tags?.includes('play-fn') - : entry.type === 'story' && !!entry.tags?.includes('play-fn') + ? entry.type !== 'story' || !entry.tags?.includes(Tag.PLAY_FN) + : entry.type === 'story' && !!entry.tags?.includes(Tag.PLAY_FN) ), }, _test: { diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts index 4a66831516fc..f41b697860fe 100644 --- a/code/core/src/manager/globals/exports.ts +++ b/code/core/src/manager/globals/exports.ts @@ -316,6 +316,7 @@ export default { 'ManagerContext', 'Provider', 'RequestResponseError', + 'Tag', 'addons', 'combineParameters', 'controlOrMetaKey', diff --git a/code/core/src/preview-api/index.ts b/code/core/src/preview-api/index.ts index 8e6b0ca382a7..f5eed4c6ad64 100644 --- a/code/core/src/preview-api/index.ts +++ b/code/core/src/preview-api/index.ts @@ -72,6 +72,8 @@ export { createPlaywrightTest, getCsfFactoryAnnotations } from './modules/store/ export type { PropDescriptor } from './store'; +export { Tag } from '../shared/constants/tags'; + /** STORIES API */ export { StoryStore, type Report, ReporterAPI } from './store'; export { diff --git a/code/core/src/preview-api/modules/preview-web/PreviewWeb.mockdata.ts b/code/core/src/preview-api/modules/preview-web/PreviewWeb.mockdata.ts index 1341774121e9..22f18372ec62 100644 --- a/code/core/src/preview-api/modules/preview-web/PreviewWeb.mockdata.ts +++ b/code/core/src/preview-api/modules/preview-web/PreviewWeb.mockdata.ts @@ -19,6 +19,7 @@ import type { import { EventEmitter } from 'events'; +import { Tag } from '../../../shared/constants/tags'; import { composeConfigs } from '../store'; import type { RenderPhase } from './render/StoryRender'; @@ -97,7 +98,7 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/ComponentOne.stories.js', storiesImports: ['./src/ExtraComponentOne.stories.js'], - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], }, 'component-one--attached-docs': { type: 'docs', @@ -106,7 +107,7 @@ export const storyIndex: StoryIndex = { name: 'Attached Docs', importPath: './src/ComponentOne.mdx', storiesImports: ['./src/ComponentOne.stories.js'], - tags: ['attached-mdx', 'docs'], + tags: [Tag.ATTACHED_MDX, 'docs'], }, 'component-one--a': { type: 'story', @@ -139,7 +140,7 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/ComponentTwo.stories.js', storiesImports: [], - tags: ['autodocs', 'docs'], + tags: [Tag.AUTODOCS, 'docs'], }, 'component-two--c': { type: 'story', @@ -156,7 +157,7 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/Introduction.mdx', storiesImports: ['./src/ComponentTwo.stories.js'], - tags: ['unattached-mdx', 'docs'], + tags: [Tag.UNATTACHED_MDX, 'docs'], }, }, }; diff --git a/code/core/src/preview-api/modules/preview-web/PreviewWithSelection.tsx b/code/core/src/preview-api/modules/preview-web/PreviewWithSelection.tsx index 2036afd75ab3..e33b90ad7f24 100644 --- a/code/core/src/preview-api/modules/preview-web/PreviewWithSelection.tsx +++ b/code/core/src/preview-api/modules/preview-web/PreviewWithSelection.tsx @@ -28,6 +28,7 @@ import type { ModuleImportFn, ProjectAnnotations } from 'storybook/internal/type import invariant from 'tiny-invariant'; +import { Tag } from '../../../shared/constants/tags'; import type { StorySpecifier } from '../store/StoryIndexStore'; import type { MaybePromise } from './Preview'; import { Preview } from './Preview'; @@ -45,13 +46,9 @@ function focusInInput(event: Event) { return /input|textarea/i.test(target.tagName) || target.getAttribute('contenteditable') !== null; } -export const AUTODOCS_TAG = 'autodocs'; -export const ATTACHED_MDX_TAG = 'attached-mdx'; -export const UNATTACHED_MDX_TAG = 'unattached-mdx'; - /** Was this docs entry generated by a .mdx file? (see discussion below) */ export function isMdxEntry({ tags }: DocsIndexEntry) { - return tags?.includes(UNATTACHED_MDX_TAG) || tags?.includes(ATTACHED_MDX_TAG); + return tags?.includes(Tag.UNATTACHED_MDX) || tags?.includes(Tag.ATTACHED_MDX); } type PossibleRender = @@ -438,7 +435,7 @@ export class PreviewWithSelection extends Preview, (_?: any) => void] => { diff --git a/code/core/src/preview-api/modules/store/csf/portable-stories.test.ts b/code/core/src/preview-api/modules/store/csf/portable-stories.test.ts index 8e1a40527a45..4abb3f1b8e8e 100644 --- a/code/core/src/preview-api/modules/store/csf/portable-stories.test.ts +++ b/code/core/src/preview-api/modules/store/csf/portable-stories.test.ts @@ -8,6 +8,7 @@ import type { StoryAnnotationsOrFn as Story, } from 'storybook/internal/types'; +import { Tag } from '../../../../shared/constants/tags'; import * as defaultExportAnnotations from './__mocks__/defaultExportAnnotations.mockfile'; import * as namedExportAnnotations from './__mocks__/namedExportAnnotations.mockfile'; import { composeStories, composeStory, setProjectAnnotations } from './portable-stories'; @@ -47,7 +48,7 @@ describe('composeStory', () => { it('should return composed project annotations via setProjectAnnotations', () => { const firstAnnotations = { parameters: { foo: 'bar' }, - tags: ['autodocs'], + tags: [Tag.AUTODOCS], }; const secondAnnotations = { @@ -60,7 +61,7 @@ describe('composeStory', () => { expect.objectContaining({ parameters: expect.objectContaining({ foo: 'bar' }), args: { foo: 'bar' }, - tags: ['autodocs'], + tags: [Tag.AUTODOCS], }) ); }); @@ -125,7 +126,7 @@ describe('composeStory', () => { expect(composedStory.parameters).toEqual( expect.objectContaining({ ...Story.parameters, ...meta.parameters }) ); - expect(composedStory.tags).toEqual(['dev', 'test', 'projectTag', 'metaTag', 'storyTag']); + expect(composedStory.tags).toEqual([Tag.DEV, Tag.TEST, 'projectTag', 'metaTag', 'storyTag']); composedStory(); diff --git a/code/core/src/preview-api/modules/store/csf/prepareStory.test.ts b/code/core/src/preview-api/modules/store/csf/prepareStory.test.ts index 69aca0b7b902..cc6a16afe383 100644 --- a/code/core/src/preview-api/modules/store/csf/prepareStory.test.ts +++ b/code/core/src/preview-api/modules/store/csf/prepareStory.test.ts @@ -15,6 +15,7 @@ import { global } from '@storybook/global'; import type { UserEventObject } from 'storybook/test'; +import { Tag } from '../../../../shared/constants/tags'; import { HooksContext, addons } from '../../addons'; import { UNTARGETED } from '../args'; import { composeConfigs } from './composeConfigs'; @@ -88,7 +89,7 @@ describe('prepareStory', () => { { render } ); - expect(tags).toEqual(['dev', 'test', 'component-1', 'component-2', 'story-1', 'story-2']); + expect(tags).toEqual([Tag.DEV, Tag.TEST, 'component-1', 'component-2', 'story-1', 'story-2']); }); it('component tags work if story are unset', () => { @@ -102,13 +103,13 @@ describe('prepareStory', () => { { render } ); - expect(tags).toEqual(['dev', 'test', 'component-1', 'component-2']); + expect(tags).toEqual([Tag.DEV, Tag.TEST, 'component-1', 'component-2']); }); it('sets a value even if annotations do not have tags', () => { const { tags } = prepareStory({ id, name, moduleExport }, { id, title }, { render }); - expect(tags).toEqual(['dev', 'test']); + expect(tags).toEqual([Tag.DEV, Tag.TEST]); }); }); diff --git a/code/core/src/preview-api/modules/store/csf/prepareStory.ts b/code/core/src/preview-api/modules/store/csf/prepareStory.ts index be45c366a0b4..bd422cbbcdb8 100644 --- a/code/core/src/preview-api/modules/store/csf/prepareStory.ts +++ b/code/core/src/preview-api/modules/store/csf/prepareStory.ts @@ -21,6 +21,7 @@ import type { import { global } from '@storybook/global'; import { global as globalThis } from '@storybook/global'; +import { Tag } from '../../../../shared/constants/tags'; import { applyHooks } from '../../addons'; import { mountDestructured } from '../../preview-web/render/mount-utils'; import { UNTARGETED, groupArgsByTarget } from '../args'; @@ -187,15 +188,15 @@ function preparePartialAnnotations( // anything at render time. The assumption is that as we don't load all the stories at once, this // will have a limited cost. If this proves misguided, we can refactor it. - const defaultTags = ['dev', 'test']; - const extraTags = globalThis.DOCS_OPTIONS?.autodocs === true ? ['autodocs'] : []; + const defaultTags = [Tag.DEV, Tag.TEST]; + const extraTags = globalThis.DOCS_OPTIONS?.autodocs === true ? [Tag.AUTODOCS] : []; /** * DISCLAIMER: This feels like a hack but seems like it's the only way to override the autodocs * tag for test-fn stories. That's because the Story index does not include negated tags e.g. * !autodocs so the negation does not get passed through, and therefore we need to do it here. * Therefore, unfortunately we have to duplicate the logic here. */ - const overrideTags = storyAnnotations?.tags?.includes('test-fn') ? ['!autodocs'] : []; + const overrideTags = storyAnnotations?.tags?.includes(Tag.TEST_FN) ? [`!${Tag.AUTODOCS}`] : []; const tags = combineTags( ...defaultTags, diff --git a/code/core/src/shared/checklist-store/checklistData.tsx b/code/core/src/shared/checklist-store/checklistData.tsx index 58f4293c8fed..c441a4bc4830 100644 --- a/code/core/src/shared/checklist-store/checklistData.tsx +++ b/code/core/src/shared/checklist-store/checklistData.tsx @@ -15,7 +15,7 @@ import { type API_StoryEntry, } from 'storybook/internal/types'; -import { type API, addons, internal_universalTestProviderStore } from 'storybook/manager-api'; +import { type API, addons, internal_universalTestProviderStore, Tag } from 'storybook/manager-api'; import { ThemeProvider, convert, styled, themes } from 'storybook/theming'; import { ADDON_ID as ADDON_A11Y_ID } from '../../../../addons/a11y/src/constants'; @@ -672,7 +672,7 @@ export default { criteria: 'At least one story with a play or test function', subscribe: subscribeToIndex((entries) => Object.values(entries).some( - (entry) => entry.tags?.includes('play-fn') || entry.tags?.includes('test-fn') + (entry) => entry.tags?.includes(Tag.PLAY_FN) || entry.tags?.includes(Tag.TEST_FN) ) ), content: ({ api }) => ( @@ -1084,7 +1084,7 @@ export const Disabled: Story = { label: 'Automatically document your components', criteria: 'At least one component with the autodocs tag applied', subscribe: subscribeToIndex((entries) => - Object.values(entries).some((entry) => entry.tags?.includes('autodocs')) + Object.values(entries).some((entry) => entry.tags?.includes(Tag.AUTODOCS)) ), content: ({ api }) => ( <> diff --git a/code/core/src/shared/constants/tags.ts b/code/core/src/shared/constants/tags.ts new file mode 100644 index 000000000000..5d5a6b11dc47 --- /dev/null +++ b/code/core/src/shared/constants/tags.ts @@ -0,0 +1,25 @@ +/** System tags used throughout Storybook for categorizing and filtering stories and docs entries. */ +export const Tag = { + /** Indicates that autodocs should be generated for this component */ + AUTODOCS: 'autodocs', + /** MDX documentation attached to a component's stories file */ + ATTACHED_MDX: 'attached-mdx', + /** Standalone MDX documentation not attached to stories */ + UNATTACHED_MDX: 'unattached-mdx', + /** Story has a play function */ + PLAY_FN: 'play-fn', + /** Story has a test function */ + TEST_FN: 'test-fn', + /** Development environment tag */ + DEV: 'dev', + /** Test environment tag */ + TEST: 'test', + /** Manifest generation tag */ + MANIFEST: 'manifest', +} as const; + +/** + * Tags can be any string, including custom user-defined tags. The Tag constant above defines the + * system tags used by Storybook. + */ +export type Tag = string; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts index 996af2ff707e..f53fde762f93 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts @@ -1,4 +1,5 @@ import { readConfig } from 'storybook/internal/csf-tools'; +import { Tag } from 'storybook/internal/core-server'; import picocolors from 'picocolors'; @@ -78,8 +79,8 @@ export const removeDocsAutodocs: Fix = { async (preview) => { const tags = preview.getFieldValue(['tags']) || []; - if (!tags.includes('autodocs') && !dryRun) { - preview.setFieldValue(['tags'], [...tags, 'autodocs']); + if (!tags.includes(Tag.AUTODOCS) && !dryRun) { + preview.setFieldValue(['tags'], [...tags, Tag.AUTODOCS]); } } ); diff --git a/code/renderers/react/src/componentManifest/fixtures.ts b/code/renderers/react/src/componentManifest/fixtures.ts index 2a3f5df215b3..35ad539d2b1d 100644 --- a/code/renderers/react/src/componentManifest/fixtures.ts +++ b/code/renderers/react/src/componentManifest/fixtures.ts @@ -1,3 +1,5 @@ +import { Tag } from 'storybook/internal/core-server'; + import { dedent } from 'ts-dedent'; export const fsMocks = { @@ -120,7 +122,7 @@ export const indexJson = { title: 'Example/Button', importPath: './src/stories/Button.stories.ts', componentPath: './src/stories/Button.tsx', - tags: ['dev', 'test', 'vitest', 'autodocs', 'manifest'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS, Tag.MANIFEST], exportName: 'Primary', }, 'example-button--secondary': { @@ -131,7 +133,7 @@ export const indexJson = { title: 'Example/Button', importPath: './src/stories/Button.stories.ts', componentPath: './src/stories/Button.tsx', - tags: ['dev', 'test', 'vitest', 'autodocs', 'manifest'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS, Tag.MANIFEST], exportName: 'Secondary', }, 'example-button--large': { @@ -142,7 +144,7 @@ export const indexJson = { title: 'Example/Button', importPath: './src/stories/Button.stories.ts', componentPath: './src/stories/Button.tsx', - tags: ['dev', 'test', 'vitest', 'autodocs', 'manifest'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS, Tag.MANIFEST], exportName: 'Large', }, 'example-button--small': { @@ -153,7 +155,7 @@ export const indexJson = { title: 'Example/Button', importPath: './src/stories/Button.stories.ts', componentPath: './src/stories/Button.tsx', - tags: ['dev', 'test', 'vitest', 'autodocs', 'manifest'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS, Tag.MANIFEST], exportName: 'Small', }, 'example-header--docs': { @@ -162,7 +164,7 @@ export const indexJson = { name: 'Docs', importPath: './src/stories/Header.stories.ts', type: 'docs', - tags: ['dev', 'test', 'vitest', 'autodocs'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS], storiesImports: [], }, 'example-header--logged-in': { @@ -173,7 +175,7 @@ export const indexJson = { title: 'Example/Header', importPath: './src/stories/Header.stories.ts', componentPath: './src/stories/Header.tsx', - tags: ['dev', 'test', 'vitest', 'autodocs', 'manifest'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS, Tag.MANIFEST], exportName: 'LoggedIn', }, 'example-header--logged-out': { @@ -184,7 +186,7 @@ export const indexJson = { title: 'Example/Header', importPath: './src/stories/Header.stories.ts', componentPath: './src/stories/Header.tsx', - tags: ['dev', 'test', 'vitest', 'autodocs', 'manifest'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS, Tag.MANIFEST], exportName: 'LoggedOut', }, }, diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index 0759a56ff156..af68047e1dcb 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -1,5 +1,7 @@ import { beforeEach, expect, test, vi } from 'vitest'; +import { Tag } from 'storybook/internal/core-server'; + import { vol } from 'memfs'; import { dedent } from 'ts-dedent'; @@ -13,7 +15,7 @@ beforeEach(() => { test('manifests generates correct id, name, description and examples ', async () => { const manifestEntries = Object.values(indexJson.entries).filter( - (entry) => entry.tags?.includes('manifest') ?? false + (entry) => entry.tags?.includes(Tag.MANIFEST) ?? false ); const result = await manifests(undefined, { manifestEntries } as any); @@ -272,7 +274,7 @@ async function getManifestForStory(code: string) { title: 'Example/Button', importPath: './src/stories/Button.stories.ts', componentPath: './src/stories/Button.tsx', - tags: ['dev', 'test', 'vitest', 'autodocs', 'manifest'], + tags: [Tag.DEV, Tag.TEST, 'vitest', Tag.AUTODOCS, Tag.MANIFEST], exportName: 'Primary', }, ]; @@ -509,7 +511,7 @@ test('should create component manifest when only attached-mdx docs have manifest name: 'Docs', title: 'Example/Button', importPath: './src/stories/Button.mdx', - tags: ['dev', 'test', 'manifest', 'attached-mdx'], + tags: [Tag.DEV, Tag.TEST, Tag.MANIFEST, Tag.ATTACHED_MDX], storiesImports: ['./src/stories/Button.stories.ts'], }, ]; diff --git a/code/renderers/react/src/componentManifest/generator.ts b/code/renderers/react/src/componentManifest/generator.ts index 12f70f6a8036..4740a407b12b 100644 --- a/code/renderers/react/src/componentManifest/generator.ts +++ b/code/renderers/react/src/componentManifest/generator.ts @@ -11,14 +11,13 @@ import { import { uniqBy } from 'es-toolkit/array'; import path from 'pathe'; +import { Tag } from 'storybook/internal/core-server'; import { getCodeSnippet } from './generateCodeSnippet'; import { getComponents, getImports } from './getComponentImports'; import { extractJSDocInfo } from './jsdocTags'; import { type DocObj } from './reactDocgen'; import { cachedFindUp, cachedReadFileSync, invalidateCache, invariant } from './utils'; -const ATTACHED_MDX_TAG = 'attached-mdx'; - interface ReactComponentManifest extends ComponentManifest { reactDocgen?: DocObj; } @@ -113,7 +112,7 @@ export const manifests: PresetPropertyFn< // addon-docs will add docs entries to these manifest entries afterwards // Docs entries have importPath pointing to MDX file, but storiesImports[0] points to the story file (entry.type === 'docs' && - entry.tags?.includes(ATTACHED_MDX_TAG) && + entry.tags?.includes(Tag.ATTACHED_MDX) && entry.storiesImports.length > 0) ), (entry) => entry.id.split('--')[0] diff --git a/code/renderers/react/src/entry-preview.tsx b/code/renderers/react/src/entry-preview.tsx index 12012e6e8891..6b4a6528f0a8 100644 --- a/code/renderers/react/src/entry-preview.tsx +++ b/code/renderers/react/src/entry-preview.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { global } from '@storybook/global'; +import { Tag } from 'storybook/internal/preview-api'; import { configure } from 'storybook/test'; import { getAct, getReactActEnvironment, setReactActEnvironment } from './act-compat'; @@ -31,7 +32,7 @@ export const decorators: Decorator[] = [ }, (story, context) => { // @ts-expect-error this feature flag only exists in the react frameworks - if (context.tags?.includes('test-fn') && !global.FEATURES?.experimentalTestSyntax) { + if (context.tags?.includes(Tag.TEST_FN) && !global.FEATURES?.experimentalTestSyntax) { throw new Error( 'To use the experimental test function, you must enable the experimentalTestSyntax feature flag. See https://storybook.js.org/docs/api/main-config/main-config-features#experimentaltestsyntax' ); From 2f7d638a91eaaffd4ffe5f4258cd38592d812bcf Mon Sep 17 00:00:00 2001 From: Jeppe Reinhold Date: Tue, 6 Jan 2026 13:14:41 +0100 Subject: [PATCH 11/11] fix lint --- code/addons/docs/src/blocks/blocks/Stories.tsx | 1 + code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx | 2 +- code/addons/vitest/src/manager.tsx | 2 +- code/core/src/csf-tools/CsfFile.ts | 2 +- code/core/src/csf/csf-factories.ts | 5 +++-- code/core/src/manager-api/lib/stories.ts | 2 +- code/core/src/manager/components/sidebar/TagsFilter.tsx | 6 +----- code/core/src/shared/checklist-store/checklistData.tsx | 2 +- .../src/automigrate/fixes/remove-docs-autodocs.ts | 2 +- code/renderers/react/src/componentManifest/generator.ts | 2 +- code/renderers/react/src/entry-preview.tsx | 3 ++- 11 files changed, 14 insertions(+), 15 deletions(-) diff --git a/code/addons/docs/src/blocks/blocks/Stories.tsx b/code/addons/docs/src/blocks/blocks/Stories.tsx index 0a23ecd66da1..e6760638d5f5 100644 --- a/code/addons/docs/src/blocks/blocks/Stories.tsx +++ b/code/addons/docs/src/blocks/blocks/Stories.tsx @@ -2,6 +2,7 @@ import type { FC, ReactElement } from 'react'; import React, { useContext } from 'react'; import { Tag } from 'storybook/internal/preview-api'; + import { styled } from 'storybook/theming'; import { DocsContext } from './DocsContext'; diff --git a/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx b/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx index b6bb7c037053..f657c2f3dc63 100644 --- a/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx +++ b/code/addons/docs/src/blocks/blocks/usePrimaryStory.test.tsx @@ -5,12 +5,12 @@ import { describe, expect, it, vi } from 'vitest'; import React from 'react'; import type { FC, PropsWithChildren } from 'react'; +import { Tag } from 'storybook/internal/core-server'; import type { PreparedStory } from 'storybook/internal/types'; import type { DocsContextProps } from './DocsContext'; import { DocsContext } from './DocsContext'; import { usePrimaryStory } from './usePrimaryStory'; -import { Tag } from 'storybook/internal/core-server'; const stories: Record> = { story1: { name: 'Story One', tags: [`!${Tag.AUTODOCS}`] }, diff --git a/code/addons/vitest/src/manager.tsx b/code/addons/vitest/src/manager.tsx index 84753db1a9aa..14e4e0815f2d 100644 --- a/code/addons/vitest/src/manager.tsx +++ b/code/addons/vitest/src/manager.tsx @@ -8,7 +8,7 @@ import { store, testProviderStore, } from '#manager-store'; -import { addons, Tag } from 'storybook/manager-api'; +import { Tag, addons } from 'storybook/manager-api'; import { GlobalErrorContext, GlobalErrorModal } from './components/GlobalErrorModal'; import { SidebarContextMenu } from './components/SidebarContextMenu'; diff --git a/code/core/src/csf-tools/CsfFile.ts b/code/core/src/csf-tools/CsfFile.ts index 34d14229d724..5663d8140537 100644 --- a/code/core/src/csf-tools/CsfFile.ts +++ b/code/core/src/csf-tools/CsfFile.ts @@ -23,9 +23,9 @@ import type { import { dedent } from 'ts-dedent'; +import { Tag } from '../shared/constants/tags'; import type { PrintResultType } from './PrintResultType'; import { findVarInitialization } from './findVarInitialization'; -import { Tag } from '../shared/constants/tags'; // We add this BabelFile as a temporary workaround to deal with a BabelFileClass "ImportEquals should have a literal source" issue in no link mode with tsup interface BabelFile { diff --git a/code/core/src/csf/csf-factories.ts b/code/core/src/csf/csf-factories.ts index d62e5ad96498..c7de8a392273 100644 --- a/code/core/src/csf/csf-factories.ts +++ b/code/core/src/csf/csf-factories.ts @@ -62,8 +62,9 @@ export function definePreview - extends ProjectAnnotations {} +export interface PreviewAddon< + in TExtraContext extends AddonTypes = AddonTypes, +> extends ProjectAnnotations {} export function definePreviewAddon( preview: ProjectAnnotations diff --git a/code/core/src/manager-api/lib/stories.ts b/code/core/src/manager-api/lib/stories.ts index ee32d35ca568..b30b77aa67db 100644 --- a/code/core/src/manager-api/lib/stories.ts +++ b/code/core/src/manager-api/lib/stories.ts @@ -26,8 +26,8 @@ import { mapValues } from 'es-toolkit/object'; import memoize from 'memoizerific'; import { dedent } from 'ts-dedent'; -import { type API, type State, combineParameters } from '../root'; import { Tag } from '../../shared/constants/tags'; +import { type API, type State, combineParameters } from '../root'; import intersect from './intersect'; import merge from './merge'; diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx index 8ea1c4fcd7f9..31d49b8c0e8f 100644 --- a/code/core/src/manager/components/sidebar/TagsFilter.tsx +++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx @@ -1,11 +1,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Badge, Button, PopoverProvider } from 'storybook/internal/components'; -import type { - API_PreparedIndexEntry, - StoryIndex, - TagsOptions, -} from 'storybook/internal/types'; +import type { API_PreparedIndexEntry, StoryIndex, TagsOptions } from 'storybook/internal/types'; import { BeakerIcon, DocumentIcon, FilterIcon, PlayHollowIcon } from '@storybook/icons'; diff --git a/code/core/src/shared/checklist-store/checklistData.tsx b/code/core/src/shared/checklist-store/checklistData.tsx index c441a4bc4830..df1a47702373 100644 --- a/code/core/src/shared/checklist-store/checklistData.tsx +++ b/code/core/src/shared/checklist-store/checklistData.tsx @@ -15,7 +15,7 @@ import { type API_StoryEntry, } from 'storybook/internal/types'; -import { type API, addons, internal_universalTestProviderStore, Tag } from 'storybook/manager-api'; +import { type API, Tag, addons, internal_universalTestProviderStore } from 'storybook/manager-api'; import { ThemeProvider, convert, styled, themes } from 'storybook/theming'; import { ADDON_ID as ADDON_A11Y_ID } from '../../../../addons/a11y/src/constants'; diff --git a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts index f53fde762f93..4229d40b42a0 100644 --- a/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts +++ b/code/lib/cli-storybook/src/automigrate/fixes/remove-docs-autodocs.ts @@ -1,5 +1,5 @@ -import { readConfig } from 'storybook/internal/csf-tools'; import { Tag } from 'storybook/internal/core-server'; +import { readConfig } from 'storybook/internal/csf-tools'; import picocolors from 'picocolors'; diff --git a/code/renderers/react/src/componentManifest/generator.ts b/code/renderers/react/src/componentManifest/generator.ts index 4740a407b12b..7c8324689bcc 100644 --- a/code/renderers/react/src/componentManifest/generator.ts +++ b/code/renderers/react/src/componentManifest/generator.ts @@ -1,4 +1,5 @@ import { recast } from 'storybook/internal/babel'; +import { Tag } from 'storybook/internal/core-server'; import { extractDescription, loadCsf } from 'storybook/internal/csf-tools'; import { logger } from 'storybook/internal/node-logger'; import type { DocsIndexEntry, IndexEntry } from 'storybook/internal/types'; @@ -11,7 +12,6 @@ import { import { uniqBy } from 'es-toolkit/array'; import path from 'pathe'; -import { Tag } from 'storybook/internal/core-server'; import { getCodeSnippet } from './generateCodeSnippet'; import { getComponents, getImports } from './getComponentImports'; import { extractJSDocInfo } from './jsdocTags'; diff --git a/code/renderers/react/src/entry-preview.tsx b/code/renderers/react/src/entry-preview.tsx index 6b4a6528f0a8..c4d912458bc2 100644 --- a/code/renderers/react/src/entry-preview.tsx +++ b/code/renderers/react/src/entry-preview.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; +import { Tag } from 'storybook/internal/preview-api'; + import { global } from '@storybook/global'; -import { Tag } from 'storybook/internal/preview-api'; import { configure } from 'storybook/test'; import { getAct, getReactActEnvironment, setReactActEnvironment } from './act-compat';