Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
06604d3
Add react-docgen-typescript to component manifest
kasperpeulen Feb 10, 2026
94d53dc
Fix positional doc-to-export mapping with name-based lookup
kasperpeulen Feb 10, 2026
596eb6b
Fix matchComponentDoc returning wrong component for barrel files
kasperpeulen Feb 10, 2026
25128ae
Filter DOM built-in props from Web Component types
kasperpeulen Feb 10, 2026
9d69201
Filter bulk CSS-in-JS system props from component manifest
kasperpeulen Feb 13, 2026
2329686
Add react-docgen-typescript as explicit dependency
kasperpeulen Feb 13, 2026
346d484
Remove overly aggressive displayName filter from RDT parser
kasperpeulen Feb 13, 2026
b6e1582
Filter system props from in-project generated .d.ts files
kasperpeulen Feb 13, 2026
9378a2a
Simplify prop filtering to single >30 threshold heuristic
kasperpeulen Feb 13, 2026
734f791
Fix lint errors and reset tsconfig on parser invalidation
kasperpeulen Feb 13, 2026
c0d26aa
Switch manifest to single docgen engine based on main.ts reactDocgen …
kasperpeulen Feb 18, 2026
c33c7ea
Clean up type cast and add docs link to manifest debugger
kasperpeulen Feb 18, 2026
bb93492
Fix review issues: surface RDT errors, remove dead code and casts, ty…
kasperpeulen Feb 19, 2026
4204040
Safe-access manifest.meta to prevent crash when manifest is undefined
kasperpeulen Feb 19, 2026
f090172
Conditionally spread reactDocgenTypescriptError (consistent with othe…
kasperpeulen Feb 19, 2026
318fe1f
Treat reactDocgen: false as react-docgen for manifest (never skip doc…
kasperpeulen Feb 19, 2026
9a0287b
Make DtsComponent test resilient to @types/react version changes
kasperpeulen Feb 19, 2026
673b98f
Revert "Make DtsComponent test resilient to @types/react version chan…
kasperpeulen Feb 19, 2026
47e714f
Fix CI: make meta optional, fix lint, rename variables
kasperpeulen Feb 19, 2026
bc885bc
Rerun CI
kasperpeulen Feb 19, 2026
679aac1
Make DtsComponent test resilient to @types/react version changes
kasperpeulen Feb 19, 2026
c094ea4
Fix CI: template name typo, reset previousProgram, remove flaky Style…
kasperpeulen Feb 19, 2026
8339724
Increase Button test timeout to 30s for slower CI machines
kasperpeulen Feb 19, 2026
db861eb
Merge branch 'next' into kasper/react-docgen-typescript
kasperpeulen Feb 19, 2026
923c2d2
Remove styled-components devDependency and test fixture
kasperpeulen Feb 19, 2026
3353c7b
Preserve previousProgram across parser invalidations and narrow meta …
kasperpeulen Feb 19, 2026
c37fa17
Disable highly flakey test
valentinpalkovic Feb 20, 2026
89cc575
Initial plan
Copilot Feb 20, 2026
5600cda
Fix Yarn version in copilot-instructions.md (4.9.1 → 4.10.3)
Copilot Feb 20, 2026
6d555c8
Rename meta.reactDocgen to meta.docgen for framework-agnostic extensi…
kasperpeulen Feb 20, 2026
3f68d91
Merge pull request #33882 from storybookjs/valentin/remove-flaky-e2e-…
valentinpalkovic Feb 20, 2026
e9cfa91
Merge pull request #33887 from storybookjs/copilot/set-up-copilot-ins…
valentinpalkovic Feb 20, 2026
228bce0
Merge pull request #33818 from storybookjs/kasper/react-docgen-typesc…
kasperpeulen Feb 20, 2026
2b4b575
Merge branch 'next-release' into next
storybook-bot Feb 23, 2026
5eb1409
fix and improve mcp addon e2e test
yannbf Feb 23, 2026
af35766
Merge pull request #33905 from storybookjs/yann/fix-mcp-e2e
yannbf Feb 23, 2026
8d80ccd
Write changelog for 10.3.0-alpha.9 [skip ci]
storybook-bot Feb 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Storybook is a large monorepo built with TypeScript, React, and various other fr
## System Requirements

- **Node.js**: 22.21.1 (see `.nvmrc`)
- **Package Manager**: Yarn 4.9.1
- **Package Manager**: Yarn 4.10.3
- **Operating System**: Linux/macOS (CI environment)

## Repository Structure
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.prerelease.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 10.3.0-alpha.9

- React: Add react-docgen-typescript to component manifest - [#33818](https://github.com/storybookjs/storybook/pull/33818), thanks @kasperpeulen!

## 10.3.0-alpha.8

- A11y: Ensure popover dialogs have an ARIA label - [#33500](https://github.com/storybookjs/storybook/pull/33500), thanks @gayanMatch!
Expand Down
168 changes: 136 additions & 32 deletions code/core/src/core-server/utils/manifests/render-components-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import path from 'node:path';

import { groupBy } from 'storybook/internal/common';

import type { ComponentDoc, PropItem } from 'react-docgen-typescript';

import type { ComponentManifest, ComponentsManifest } from '../../../types';

/** Minimal docs entry type for rendering in the manifest debugger */
Expand Down Expand Up @@ -56,6 +58,9 @@ export function renderComponentsManifest(
docsWithError: unattachedDocsWithError + attachedDocsWithError,
};

const activeEngine = manifest?.meta?.docgen ?? 'react-docgen';
const durationMs = manifest?.meta?.durationMs ?? 0;

// Top filters (clickable), no <b> tags; 1px active ring lives in CSS via :target
const allPill = `<a class="filter-pill all" data-k="all" href="#filter-all">All</a>`;
const compErrorsPill =
Expand Down Expand Up @@ -695,6 +700,28 @@ export function renderComponentsManifest(
</header>
<main>
<div class="wrap">
${
activeEngine === 'react-docgen'
? `<div class="note info" style="margin-bottom: 16px;">
<strong>Tip:</strong> You are using <code>react-docgen</code> (the default). Generation took <strong>${(durationMs / 1000).toFixed(1)}s</strong>. For higher quality prop types, consider switching to <code>react-docgen-typescript</code> in your <code>main.ts</code>:
<pre><code>typescript: {
reactDocgen: 'react-docgen-typescript',
}</code></pre>
Note: <code>react-docgen-typescript</code> can be slower. If performance is acceptable for your project, it generally produces better results.
<a href="https://storybook.js.org/docs/api/main-config/main-config-typescript#reactdocgen" target="_blank">Learn more</a>
</div>`
: activeEngine === 'react-docgen-typescript' && durationMs > 7500
? `<div class="note err" style="margin-bottom: 16px;">
<strong>Performance warning:</strong> <code>react-docgen-typescript</code> took <strong>${(durationMs / 1000).toFixed(1)}s</strong> to generate the manifest. This delay applies every time the manifest is used by an agent. Consider switching to the faster <code>react-docgen</code> in your <code>main.ts</code>:
<pre><code>typescript: {
reactDocgen: 'react-docgen',
}</code></pre>
<a href="https://storybook.js.org/docs/api/main-config/main-config-typescript#reactdocgen" target="_blank">Learn more</a>
</div>`
: `<div class="note ok" style="margin-bottom: 16px;">
Using <code>${activeEngine}</code>. Generation took <strong>${(durationMs / 1000).toFixed(1)}s</strong>.
</div>`
}
${
grid
? `<h2 class="section-title">Components</h2>
Expand Down Expand Up @@ -887,10 +914,27 @@ function renderComponentCard(key: string, c: ComponentManifestWithDocs, id: stri
? `<label for="${slug}-docs" class="badge ${a.docsErrors > 0 ? 'err' : 'ok'} as-toggle">${a.docsErrors > 0 ? `${a.docsErrors}/${a.totalDocs} doc errors` : `${a.totalDocs} ${plural(a.totalDocs, 'doc')}`}</label>`
: '';

// When there is no prop type error, try to read prop types from reactDocgen if present
const reactDocgen: any = !a.hasPropTypeError && 'reactDocgen' in c && c.reactDocgen;
// Determine which docgen engine produced results (they are now mutually exclusive)
const reactDocgen =
!a.hasPropTypeError && 'reactDocgen' in c ? (c.reactDocgen as DocgenDoc) : undefined;
const reactDocgenTypescriptData =
!a.hasPropTypeError && 'reactDocgenTypescript' in c
? (c.reactDocgenTypescript as RdtComponentDoc)
: undefined;

const parsedDocgen = reactDocgen ? parseReactDocgen(reactDocgen) : undefined;
const propEntries = parsedDocgen ? Object.entries(parsedDocgen.props ?? {}) : [];
const parsedReactDocgenTypescript = reactDocgenTypescriptData
? parseReactDocgenTypescript(reactDocgenTypescriptData)
: undefined;

// Use whichever engine is active
const activeParsed = parsedDocgen ?? parsedReactDocgenTypescript;
const cardEngine = parsedDocgen
? 'react-docgen'
: parsedReactDocgenTypescript
? 'react-docgen-typescript'
: '';
const propEntries = activeParsed ? Object.entries(activeParsed.props ?? {}) : [];
const propTypesBadge =
!a.hasPropTypeError && propEntries.length > 0
? `<label for="${slug}-props" class="badge ok as-toggle">${propEntries.length} ${plural(propEntries.length, 'prop type')}</label>`
Expand Down Expand Up @@ -927,7 +971,6 @@ function renderComponentCard(key: string, c: ComponentManifestWithDocs, id: stri
.join('')
: '';

esc(c.error?.message || 'Unknown error');
return `
<article
class="card
Expand Down Expand Up @@ -983,10 +1026,22 @@ function renderComponentCard(key: string, c: ComponentManifestWithDocs, id: stri
<div class="panel panel-props">
<div class="note ok">
<div class="row">
<span class="ex-name">Prop types</span>
<span class="ex-name">Prop types <small>(${cardEngine})</small></span>
<span class="badge ok">${propEntries.length} ${plural(propEntries.length, 'prop type')}</span>
</div>
<pre><code>Component: ${reactDocgen?.definedInFile ? esc(path.relative(process.cwd(), reactDocgen.definedInFile)) : ''}${reactDocgen?.exportName ? '::' + esc(reactDocgen?.exportName) : ''}</code></pre>
<pre><code>Component: ${
reactDocgen?.definedInFile
? esc(path.relative(process.cwd(), reactDocgen.definedInFile))
: reactDocgenTypescriptData?.filePath
? esc(path.relative(process.cwd(), reactDocgenTypescriptData.filePath))
: ''
}${
reactDocgen?.exportName
? '::' + esc(reactDocgen.exportName)
: reactDocgenTypescriptData?.exportName
? '::' + esc(reactDocgenTypescriptData.exportName)
: ''
}</code></pre>
<pre><code>Props:</code></pre>
<pre><code>${esc(propsCode)}</code></pre>
</div>
Expand Down Expand Up @@ -1081,20 +1136,70 @@ function renderComponentCard(key: string, c: ComponentManifestWithDocs, id: stri
</article>`;
}

type ParsedProp = {
description?: string;
type?: string;
defaultValue?: string;
required?: boolean;
};

type ParsedDocgen = {
props: Record<
string,
{
description?: string;
type?: string;
defaultValue?: string;
required?: boolean;
}
>;
props: Record<string, ParsedProp>;
};

type RdtComponentDoc = ComponentDoc & { exportName?: string };

const parseReactDocgenTypescript = (reactDocgenTypescript: RdtComponentDoc): ParsedDocgen => {
const props: Record<string, PropItem> = reactDocgenTypescript.props ?? {};
return {
props: Object.fromEntries(
Object.entries(props).map(([propName, prop]) => [
propName,
{
description: prop.description,
// RDT uses prop.type.name as a flat string (e.g. "() => void", "{ id: string }")
// For enums, prefer prop.type.raw which has the full union
type: prop.type?.raw ?? prop.type?.name,
defaultValue: prop.defaultValue?.value,
required: prop.required,
},
])
),
};
};

const parseReactDocgen = (reactDocgen: any): ParsedDocgen => {
const props: Record<string, any> = (reactDocgen as any)?.props ?? {};
/** Shape of a react-docgen tsType node (recursive) */
interface DocgenTsType {
name?: string;
raw?: string;
value?: string;
elements?: DocgenTsType[];
type?: string;
signature?: {
arguments?: { name: string; type?: DocgenTsType }[];
return?: DocgenTsType;
properties?: { key: string; value?: DocgenTsType & { required?: boolean } }[];
};
}

/** Shape of a single prop from react-docgen's Documentation.props */
interface DocgenPropItem {
description?: string;
tsType?: DocgenTsType;
type?: DocgenTsType;
defaultValue?: { value?: string } | null;
required?: boolean;
}

/** Shape of react-docgen's Documentation (only fields we read) */
interface DocgenDoc {
props?: Record<string, DocgenPropItem>;
definedInFile?: string;
exportName?: string;
}

const parseReactDocgen = (reactDocgen: DocgenDoc): ParsedDocgen => {
const props = reactDocgen.props ?? {};
return {
props: Object.fromEntries(
Object.entries(props).map(([propName, prop]) => [
Expand All @@ -1111,54 +1216,53 @@ const parseReactDocgen = (reactDocgen: any): ParsedDocgen => {
};

// Serialize a react-docgen tsType into a TypeScript-like string when raw is not available
function serializeTsType(tsType: any): string | undefined {
function serializeTsType(tsType: DocgenTsType | undefined): string | undefined {
if (!tsType) {
return undefined;
}
// Prefer raw if provided
// Prefer raw if provided
if ('raw' in tsType && typeof tsType.raw === 'string' && tsType.raw.trim().length > 0) {
if (tsType.raw && tsType.raw.trim().length > 0) {
return tsType.raw;
}

if (!tsType.name) {
return undefined;
}

if ('elements' in tsType) {
if (tsType.elements) {
if (tsType.name === 'union') {
const parts = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown');
const parts = tsType.elements.map((el) => serializeTsType(el) ?? 'unknown');
return parts.join(' | ');
}
if (tsType.name === 'intersection') {
const parts = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown');
const parts = tsType.elements.map((el) => serializeTsType(el) ?? 'unknown');
return parts.join(' & ');
}
if (tsType.name === 'Array') {
// Prefer raw earlier; here build fallback
const el = (tsType.elements ?? [])[0];
const el = tsType.elements[0];
const inner = serializeTsType(el) ?? 'unknown';
return `${inner}[]`;
}
if (tsType.name === 'tuple') {
const parts = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown');
const parts = tsType.elements.map((el) => serializeTsType(el) ?? 'unknown');
return `[${parts.join(', ')}]`;
}
}
if ('value' in tsType && tsType.name === 'literal') {
if (tsType.value && tsType.name === 'literal') {
return tsType.value;
}
if ('signature' in tsType && tsType.name === 'signature') {
if (tsType.signature && tsType.name === 'signature') {
if (tsType.type === 'function') {
const args = (tsType.signature?.arguments ?? []).map((a: any) => {
const args = (tsType.signature.arguments ?? []).map((a) => {
const argType = serializeTsType(a.type) ?? 'any';
return `${a.name}: ${argType}`;
});
const ret = serializeTsType(tsType.signature?.return) ?? 'void';
const ret = serializeTsType(tsType.signature.return) ?? 'void';
return `(${args.join(', ')}) => ${ret}`;
}
if (tsType.type === 'object') {
const props = (tsType.signature?.properties ?? []).map((p: any) => {
const props = (tsType.signature.properties ?? []).map((p) => {
const req: boolean = Boolean(p.value?.required);
const propType = serializeTsType(p.value) ?? 'any';
return `${p.key}${req ? '' : '?'}: ${propType}`;
Expand All @@ -1168,8 +1272,8 @@ function serializeTsType(tsType: any): string | undefined {
return 'unknown';
}
// Default case (Generic like Item<TMeta>)
if ('elements' in tsType) {
const inner = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown');
if (tsType.elements) {
const inner = tsType.elements.map((el) => serializeTsType(el) ?? 'unknown');

if (inner.length > 0) {
return `${tsType.name}<${inner.join(', ')}>`;
Expand Down
4 changes: 4 additions & 0 deletions code/core/src/types/modules/core-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@ export interface ComponentManifest {
export interface ComponentsManifest {
v: number;
components: Record<string, ComponentManifest>;
meta?: {
docgen: 'react-docgen' | 'react-docgen-typescript';
durationMs: number;
};
}

type ManifestName = string;
Expand Down
16 changes: 12 additions & 4 deletions code/e2e-tests/addon-mcp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,6 @@ test.describe('addon-mcp', () => {
test('should show both toolsets as enabled', async ({ page }) => {
await page.goto(MCP_ENDPOINT);

// Both toolsets should show as enabled
const enabledStatuses = page.locator('.toolset-status.enabled');
await expect(enabledStatuses).toHaveCount(2);

// Check that dev toolset is listed with its tools
const devToolset = page.locator('.toolset', { has: page.locator('text=dev') });
await expect(devToolset).toBeVisible();
Expand All @@ -147,6 +143,18 @@ test.describe('addon-mcp', () => {
const docsToolset = page.locator('.toolset', { has: page.locator('text=docs') });
await expect(docsToolset).toBeVisible();
await expect(docsToolset.locator('.toolset-status')).toHaveText('enabled');

// Check that test toolset is listed with its tools
const testToolset = page.locator('.toolset', { has: page.locator('text=test') });
await expect(testToolset).toBeVisible();
await expect(testToolset.locator('.toolset-status').first()).toHaveText('enabled');

// Check that accessibility tool is enabled
const accessibilityTool = testToolset.locator(
'.toolset-tools li:has-text("accessibility")'
);
await expect(accessibilityTool).toBeVisible();
await expect(accessibilityTool.locator('.toolset-status')).toHaveText('+ accessibility');
});
});

Expand Down
3 changes: 2 additions & 1 deletion code/e2e-tests/preview-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ test.describe('preview-api', () => {
});

// if rerenders were interleaved the button would have rendered "Error: Interleaved loaders. Changed arg"
test('should only render once at a time when rapidly changing args', async ({ page }) => {
// TODO: ENABLE again once the flake is fixed
test.skip('should only render once at a time when rapidly changing args', async ({ page }) => {
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory('core/rendering', 'slow-loader');

Expand Down
3 changes: 2 additions & 1 deletion code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,5 +220,6 @@
"Dependency Upgrades"
]
]
}
},
"deferredNextVersion": "10.3.0-alpha.9"
}
3 changes: 2 additions & 1 deletion code/renderers/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"dependencies": {
"@storybook/global": "^5.0.0",
"@storybook/react-dom-shim": "workspace:*",
"react-docgen": "^8.0.2"
"react-docgen": "^8.0.2",
"react-docgen-typescript": "^2.2.2"
},
"devDependencies": {
"@types/babel-plugin-react-docgen": "^4.2.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface CardProps {
title: string;
}
export const Card = (props: CardProps) => null;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface ButtonProps {
label: string;
disabled?: boolean;
}
export function Button(props: ButtonProps) {
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
interface IconProps {
name: string;
size?: number;
}
function Icon(props: IconProps) {
return null;
}
export default Icon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface AlertProps {
message: string;
severity?: string;
}
export function Alert({ message, severity = 'info' }: AlertProps) {
return null;
}
Loading