From 503017e339f6ce80f35d72ebe7e56dbcdf22b203 Mon Sep 17 00:00:00 2001 From: frontandrews Date: Thu, 2 Apr 2026 21:10:46 -0300 Subject: [PATCH 1/3] React-Vite: Add reactDocgenOptions for built-in react-docgen plugin --- code/frameworks/react-vite/src/preset.test.ts | 69 +++++++++++++++++++ code/frameworks/react-vite/src/preset.ts | 10 +-- code/frameworks/react-vite/src/types.ts | 5 ++ ...-config-typescript-react-docgen-options.md | 33 +++++++++ .../main-config/main-config-typescript.mdx | 18 +++++ docs/configure/integration/typescript.mdx | 1 + 6 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 code/frameworks/react-vite/src/preset.test.ts create mode 100644 docs/_snippets/main-config-typescript-react-docgen-options.md diff --git a/code/frameworks/react-vite/src/preset.test.ts b/code/frameworks/react-vite/src/preset.test.ts new file mode 100644 index 000000000000..08b470ccbb57 --- /dev/null +++ b/code/frameworks/react-vite/src/preset.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + reactDocgen: vi.fn(), +})); + +vi.mock('./plugins/react-docgen.ts', () => ({ + reactDocgen: mocks.reactDocgen, +})); + +import { viteFinal } from './preset.ts'; + +describe('react-vite preset', () => { + beforeEach(() => { + mocks.reactDocgen.mockReset().mockResolvedValue({ name: 'storybook:react-docgen-plugin' }); + }); + + it('passes reactDocgenOptions to the react-docgen plugin', async () => { + const existingPlugin = { name: 'existing-plugin' }; + const reactDocgenOptions = { + exclude: [/node_modules\/.*/, /packages\/.*/], + }; + + const config = await viteFinal({ plugins: [existingPlugin] }, { + presets: { + apply: async (name: string) => { + if (name === 'typescript') { + return { + reactDocgen: 'react-docgen', + reactDocgenOptions, + }; + } + + return undefined; + }, + }, + } as any); + + expect(mocks.reactDocgen).toHaveBeenCalledWith({ + include: /\.(mjs|tsx?|jsx?)$/, + ...reactDocgenOptions, + }); + expect(config.plugins).toEqual([{ name: 'storybook:react-docgen-plugin' }, existingPlugin]); + }); + + it('allows reactDocgenOptions to override the default include pattern', async () => { + const reactDocgenOptions = { + include: /\.jsx$/, + exclude: [/packages\/.*/], + }; + + await viteFinal({}, { + presets: { + apply: async (name: string) => { + if (name === 'typescript') { + return { + reactDocgen: 'react-docgen', + reactDocgenOptions, + }; + } + + return undefined; + }, + }, + } as any); + + expect(mocks.reactDocgen).toHaveBeenCalledWith(reactDocgenOptions); + }); +}); diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts index 1f752d510376..054434b2c627 100644 --- a/code/frameworks/react-vite/src/preset.ts +++ b/code/frameworks/react-vite/src/preset.ts @@ -11,10 +11,11 @@ export const viteFinal: NonNullable = async (confi const plugins = [...(config?.plugins ?? [])]; // Add docgen plugin - const { reactDocgen: reactDocgenOption, reactDocgenTypescriptOptions } = await presets.apply( - 'typescript', - {} - ); + const { + reactDocgen: reactDocgenOption, + reactDocgenOptions, + reactDocgenTypescriptOptions, + } = await presets.apply('typescript', {}); let typescriptPresent; try { @@ -42,6 +43,7 @@ export const viteFinal: NonNullable = async (confi // If react-docgen is specified, use it for everything, otherwise only use it for non-typescript files await reactDocgen({ include: reactDocgenOption === 'react-docgen' ? /\.(mjs|tsx?|jsx?)$/ : /\.(mjs|jsx?)$/, + ...reactDocgenOptions, }) ); } diff --git a/code/frameworks/react-vite/src/types.ts b/code/frameworks/react-vite/src/types.ts index 938139710843..e402e8dc5316 100644 --- a/code/frameworks/react-vite/src/types.ts +++ b/code/frameworks/react-vite/src/types.ts @@ -7,6 +7,7 @@ import type { import type { BuilderOptions, StorybookConfigVite } from '@storybook/builder-vite'; import type docgenTypescript from '@joshwooding/vite-plugin-react-docgen-typescript'; +import type { reactDocgen } from './plugins/react-docgen.ts'; type FrameworkName = CompatibleString<'@storybook/react-vite'>; type BuilderName = CompatibleString<'@storybook/builder-vite'>; @@ -26,6 +27,8 @@ export type FrameworkOptions = { legacyRootApi?: boolean; }; +export type ReactDocgenOptions = Parameters[0]; + type StorybookConfigFramework = { framework: | FrameworkName @@ -58,6 +61,8 @@ type TypescriptOptions = TypescriptOptionsBase & { * @default `'react-docgen'` */ reactDocgen: 'react-docgen-typescript' | 'react-docgen' | false; + /** Configures the built-in Vite `react-docgen` plugin */ + reactDocgenOptions: ReactDocgenOptions; /** Configures `@joshwooding/vite-plugin-react-docgen-typescript` */ reactDocgenTypescriptOptions: Parameters[0]; }; diff --git a/docs/_snippets/main-config-typescript-react-docgen-options.md b/docs/_snippets/main-config-typescript-react-docgen-options.md new file mode 100644 index 000000000000..0366465e9470 --- /dev/null +++ b/docs/_snippets/main-config-typescript-react-docgen-options.md @@ -0,0 +1,33 @@ +```ts filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF 3" +// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc. +import type { StorybookConfig } from '@storybook/your-framework'; + +const config: StorybookConfig = { + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + typescript: { + reactDocgen: 'react-docgen', + reactDocgenOptions: { + exclude: [/node_modules\/.*/, /packages\/.*/], + }, + }, +}; + +export default config; +``` + +```ts filename=".storybook/main.ts" renderer="react" language="ts" tabTitle="CSF Next 🧪" +// Replace your-framework with the framework you are using (e.g., react-vite, nextjs, nextjs-vite) +import { defineMain } from '@storybook/your-framework/node'; + +export default defineMain({ + framework: '@storybook/your-framework', + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + typescript: { + reactDocgen: 'react-docgen', + reactDocgenOptions: { + exclude: [/node_modules\/.*/, /packages\/.*/], + }, + }, +}); +``` diff --git a/docs/api/main-config/main-config-typescript.mdx b/docs/api/main-config/main-config-typescript.mdx index c23f8e693a77..89d4a6d4f795 100644 --- a/docs/api/main-config/main-config-typescript.mdx +++ b/docs/api/main-config/main-config-typescript.mdx @@ -15,6 +15,10 @@ Type: check?: boolean; checkOptions?: CheckOptions; reactDocgen?: 'react-docgen' | 'react-docgen-typescript' | false; + reactDocgenOptions?: { + include?: string | RegExp | (string | RegExp)[]; + exclude?: string | RegExp | (string | RegExp)[]; + }; reactDocgenTypescriptOptions?: ReactDocgenTypescriptOptions; skipCompiler?: boolean; } @@ -75,6 +79,20 @@ Options to pass to `fork-ts-checker-webpack-plugin`, if [enabled](#check). See [ {/* prettier-ignore-end */} + ## `reactDocgenOptions` + + Type: `{ include?: string | RegExp | (string | RegExp)[]; exclude?: string | RegExp | (string | RegExp)[] }` + + Available for Vite-based React projects when `reactDocgen` is enabled. + + Configures the options passed to Storybook's built-in `react-docgen` Vite plugin, which can be useful in monorepos where you need to narrow the files that Storybook analyzes. + + {/* prettier-ignore-start */} + + + + {/* prettier-ignore-end */} + ## `reactDocgenTypescriptOptions` Type: `ReactDocgenTypescriptOptions` diff --git a/docs/configure/integration/typescript.mdx b/docs/configure/integration/typescript.mdx index ae0a3abf9f4d..59cfadbab450 100644 --- a/docs/configure/integration/typescript.mdx +++ b/docs/configure/integration/typescript.mdx @@ -43,6 +43,7 @@ See the [main configuration API reference](../../api/main-config/main-config.mdx | `check` | Available for Webpack-based projects.
Enables type checking within Storybook
`typescript: { check: true },` | | `checkOptions` | Requires the `check` option to be enabled.
Configures the [`fork-ts-checker-webpack-plugin`](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin) plugin
`typescript: { checkOptions: {},},` | | `reactDocgen` | Configures the TypeScript parser used by Storybook.
Available options: `react-docgen` (default), `react-docgen-typescript`,` false`
`typescript: { reactDocgen: 'react-docgen'},` | + | `reactDocgenOptions` | Available for Vite-based React projects when `reactDocgen` is enabled.
Configures Storybook's built-in `react-docgen` Vite plugin, including custom `include` and `exclude` filters.
`typescript: { reactDocgen: 'react-docgen', reactDocgenOptions: {},},` | | `reactDocgenTypescriptOptions` | Requires the `reactDocgen`option to be `react-docgen-typescript`.
Configures the `react-docgen-typescript-plugin` plugin per builder
`typescript: { reactDocgen: 'react-docgen-typescript', reactDocgenTypescriptOptions: {},},` | | `skipCompiler` | Disables parsing Typescript files through the compiler
`typescript: { skipCompiler:false,},` | From 030b0c88418be0318ec131d5c5a9904d3472974c Mon Sep 17 00:00:00 2001 From: frontandrews Date: Thu, 2 Apr 2026 22:24:52 -0300 Subject: [PATCH 2/3] React-Vite: Align preset typing and test spies --- code/frameworks/react-vite/src/preset.test.ts | 17 +++++++---------- code/frameworks/react-vite/src/preset.ts | 7 ++++++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/code/frameworks/react-vite/src/preset.test.ts b/code/frameworks/react-vite/src/preset.test.ts index 08b470ccbb57..f669e2d81761 100644 --- a/code/frameworks/react-vite/src/preset.test.ts +++ b/code/frameworks/react-vite/src/preset.test.ts @@ -1,18 +1,15 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -const mocks = vi.hoisted(() => ({ - reactDocgen: vi.fn(), -})); - -vi.mock('./plugins/react-docgen.ts', () => ({ - reactDocgen: mocks.reactDocgen, -})); +vi.mock(import('./plugins/react-docgen.ts'), { spy: true }); +import * as reactDocgenModule from './plugins/react-docgen.ts'; import { viteFinal } from './preset.ts'; describe('react-vite preset', () => { beforeEach(() => { - mocks.reactDocgen.mockReset().mockResolvedValue({ name: 'storybook:react-docgen-plugin' }); + vi.mocked(reactDocgenModule.reactDocgen) + .mockReset() + .mockResolvedValue({ name: 'storybook:react-docgen-plugin' }); }); it('passes reactDocgenOptions to the react-docgen plugin', async () => { @@ -36,7 +33,7 @@ describe('react-vite preset', () => { }, } as any); - expect(mocks.reactDocgen).toHaveBeenCalledWith({ + expect(reactDocgenModule.reactDocgen).toHaveBeenCalledWith({ include: /\.(mjs|tsx?|jsx?)$/, ...reactDocgenOptions, }); @@ -64,6 +61,6 @@ describe('react-vite preset', () => { }, } as any); - expect(mocks.reactDocgen).toHaveBeenCalledWith(reactDocgenOptions); + expect(reactDocgenModule.reactDocgen).toHaveBeenCalledWith(reactDocgenOptions); }); }); diff --git a/code/frameworks/react-vite/src/preset.ts b/code/frameworks/react-vite/src/preset.ts index 054434b2c627..5c71a5e79835 100644 --- a/code/frameworks/react-vite/src/preset.ts +++ b/code/frameworks/react-vite/src/preset.ts @@ -7,6 +7,11 @@ export const core: PresetProperty<'core'> = { renderer: import.meta.resolve('@storybook/react/preset'), }; +type TypescriptPresetOptions = Pick< + NonNullable, + 'reactDocgen' | 'reactDocgenOptions' | 'reactDocgenTypescriptOptions' +>; + export const viteFinal: NonNullable = async (config, { presets }) => { const plugins = [...(config?.plugins ?? [])]; @@ -15,7 +20,7 @@ export const viteFinal: NonNullable = async (confi reactDocgen: reactDocgenOption, reactDocgenOptions, reactDocgenTypescriptOptions, - } = await presets.apply('typescript', {}); + } = await presets.apply('typescript', {}); let typescriptPresent; try { From 29326aabf5036d6eeeb54df39b5cd95f96ac6243 Mon Sep 17 00:00:00 2001 From: frontandrews Date: Thu, 2 Apr 2026 23:13:03 -0300 Subject: [PATCH 3/3] React-Vite: Extract repeated test preset setup --- code/frameworks/react-vite/src/preset.test.ts | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/code/frameworks/react-vite/src/preset.test.ts b/code/frameworks/react-vite/src/preset.test.ts index f669e2d81761..216d1db83147 100644 --- a/code/frameworks/react-vite/src/preset.test.ts +++ b/code/frameworks/react-vite/src/preset.test.ts @@ -5,6 +5,18 @@ vi.mock(import('./plugins/react-docgen.ts'), { spy: true }); import * as reactDocgenModule from './plugins/react-docgen.ts'; import { viteFinal } from './preset.ts'; +type ViteFinalOptions = Parameters[1]; + +const createTypescriptPresetOptions = ( + reactDocgenOptions: Record +): ViteFinalOptions => + ({ + presets: { + apply: async (name: string) => + name === 'typescript' ? { reactDocgen: 'react-docgen', reactDocgenOptions } : undefined, + }, + }) as ViteFinalOptions; + describe('react-vite preset', () => { beforeEach(() => { vi.mocked(reactDocgenModule.reactDocgen) @@ -18,20 +30,10 @@ describe('react-vite preset', () => { exclude: [/node_modules\/.*/, /packages\/.*/], }; - const config = await viteFinal({ plugins: [existingPlugin] }, { - presets: { - apply: async (name: string) => { - if (name === 'typescript') { - return { - reactDocgen: 'react-docgen', - reactDocgenOptions, - }; - } - - return undefined; - }, - }, - } as any); + const config = await viteFinal( + { plugins: [existingPlugin] }, + createTypescriptPresetOptions(reactDocgenOptions) + ); expect(reactDocgenModule.reactDocgen).toHaveBeenCalledWith({ include: /\.(mjs|tsx?|jsx?)$/, @@ -46,20 +48,7 @@ describe('react-vite preset', () => { exclude: [/packages\/.*/], }; - await viteFinal({}, { - presets: { - apply: async (name: string) => { - if (name === 'typescript') { - return { - reactDocgen: 'react-docgen', - reactDocgenOptions, - }; - } - - return undefined; - }, - }, - } as any); + await viteFinal({}, createTypescriptPresetOptions(reactDocgenOptions)); expect(reactDocgenModule.reactDocgen).toHaveBeenCalledWith(reactDocgenOptions); });