Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
6d555c8
Rename meta.reactDocgen to meta.docgen for framework-agnostic extensi…
kasperpeulen Feb 20, 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
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 @@ -371,6 +371,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
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"
Comment thread
JReinhold marked this conversation as resolved.
},
"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;
}
Comment thread
kasperpeulen marked this conversation as resolved.
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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { FC } from 'react';

interface ModalProps {
title: string;
open?: boolean;
}

// Component has an explicit displayName that differs from the variable name
const InternalModal: FC<ModalProps> = (props) => null as any;
InternalModal.displayName = 'FancyModal';

export { InternalModal as Modal };
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
interface TooltipProps {
/** The content to display */
content: string;
}
/** A tooltip component. */
export function Tooltip(props: TooltipProps) {
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ButtonHTMLAttributes } from 'react';

interface HtmlButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
/** The button variant */
variant?: 'solid' | 'outline';
}

export function HtmlButton(props: HtmlButtonProps) {
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { forwardRef } from 'react';

interface TextInputProps {
/** Input label */
label: string;
/** Placeholder text */
placeholder?: string;
/** Change handler */
onChange?: (value: string) => void;
}

export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
function TextInput(props, ref) {
return null;
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
interface CallbackProps {
onClick?: (id: string) => void;
onSubmit: () => boolean;
}
export function Callback(props: CallbackProps) {
return null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
interface ListProps<T> {
items: T[];
renderItem: (item: T) => string;
emptyMessage?: string;
}

export function StringList(props: ListProps<string>) {
return null;
}

export function NumberList(props: ListProps<number>) {
return null;
}
Loading
Loading