React: Add react-docgen-typescript to component manifest#33818
React: Add react-docgen-typescript to component manifest#33818kasperpeulen merged 27 commits intonextfrom
Conversation
Integrate react-docgen-typescript alongside existing react-docgen to provide TypeScript-aware prop extraction in the component manifest. This gives more accurate type information for components using TypeScript features like generics, Pick/Omit, intersection types, and re-exports. Key changes: - New reactDocgenTypescript.ts module with TS program management, prop filtering (strips React built-in Attributes), and export name resolution - Wire rdt into getComponentImports alongside existing react-docgen - Add invalidateParser() for fresh TS program on each manifest request - Extract findTsconfigPath to shared utils (removed dead empathic/find import) - Type render-components-manifest.ts with rdt types, add rdt debug panel - Add styled-components as devDependency for test fixtures - 18 new test cases covering Button, Arrow, DefaultExport, MultipleExports, UnionProps, FunctionProps, DefaultValues, Documented, NoComponents, ImportedProps, PickOmit, Generic, ReExport, Intersection, DtsComponent, StyledComponent, ForwardRef, and Barrel exports
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds react-docgen-typescript (RDT) support across the React component manifest pipeline and core manifest renderer: new cached/incremental RDT parser, manifest wiring to include RDT docs, UI toggle/badge/panel for RDT props, TS-type serialization updates, many TS/TSX test fixtures, and tsconfig lookup improvements. Changes
Sequence DiagramsequenceDiagram
participant Collector as Component Collector
participant Cache as Parser Cache
participant Parser as RDT Parser
participant TS as TypeScript Program
participant RDT as react-docgen-typescript
Collector->>Cache: request parse(filePath)
Cache-->>Cache: lookup cached result
alt cache hit
Cache->>Collector: return cached docs
else cache miss
Cache->>Parser: parse file
Parser->>TS: create/reuse TS program (incremental)
Parser->>RDT: invoke react-docgen-typescript
RDT->>TS: query symbols/types
RDT-->>Parser: return raw ComponentDoc[]
Parser->>Parser: enrich docs (exportName, filter builtins)
Parser-->>Cache: store ComponentDocWithExportName[]
Parser->>Collector: return parsed docs
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
`@code/renderers/react/src/componentManifest/__testfixtures__/DefaultExport.ts`:
- Around line 5-7: The function Icon declares an unused parameter props which
triggers ESLint no-unused-vars; fix by renaming the parameter to a prefixed
unused name (e.g., _props) in the Icon function signature so lint ignores it,
updating the Icon(props: IconProps) declaration accordingly; ensure only the
parameter name changes (keep IconProps type) so the test fixture behavior and
type extraction remain the same.
In `@code/renderers/react/src/componentManifest/reactDocgenTypescript.ts`:
- Around line 83-117: invalidateParser() only resets the shared parser object
but not the per-file memoization used by parseWithReactDocgenTypescript, causing
stale results; add a cache-buster counter (e.g., parserInvalidateCounter) that
invalidateParser() increments and include that counter in the memoization key
for parseWithReactDocgenTypescript (change the memo key from just filePath to
something like `${filePath}:${parserInvalidateCounter}`) so cached entries are
ignored after invalidation; ensure the counter is initialized near
parser/previousProgram and referenced where the memoized lookup occurs.
Replace the fragile positional mapping (exportNames[i]) with a name-based
approach using getExportNameMap. This correctly handles:
- Aliased re-exports: `export { Card as RenamedCard }`
- Default exports: displayName derived from filename
- displayName overrides: `Foo.displayName = 'Bar'`
Add test fixtures and tests for RenamedExport and DisplayNameOverride.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@code/renderers/react/src/componentManifest/reactDocgenTypescript.test.ts`:
- Around line 9-18: The normalize function's replacer only strips Unix-style
separators so Windows paths remain; update the regex used in the JSON.stringify
replacer (the check for (key === 'filePath' || key === 'fileName') && typeof
value === 'string') to match both forward and backslashes (e.g. use a character
class for / and \) so __testfixtures__\... and __testfixtures__/... are both
stripped; keep the rest of the JSON.stringify-based normalization and only
change the regex used in normalize.
In `@code/renderers/react/src/componentManifest/reactDocgenTypescript.ts`:
- Around line 66-68: The code deriving fileName from sourceFile.fileName using
replace(/.*\//, '').replace(/\.[^.]+$/, '') is not cross-platform and fails on
Windows; update the logic in reactDocgenTypescript where fileName is computed
(referencing result, fileName, and sourceFile.fileName) to use Node's path
utilities (e.g., path.basename and path.extname) so you strip directories and
remove the extension in a platform-independent way, ensuring the default-export
displayName mapping works on Windows too.
When no matching doc was found in a multi-export file, the fallback `?? docs[0]` would silently return the first component (e.g. BaseStyles from src/index.ts). Return undefined instead so wrong data is never shown in the manifest.
Expand the prop filter to also exclude props inherited from DOM built-in interfaces (HTMLElement, Node, Element, GlobalEventHandlers, etc.) via lib.dom.d.ts. These leak through when wrapping Web Components like @github/relative-time-element, reducing RelativeTime from 331 to 27 props. Also tighten the React filter: check both Attributes name AND node_modules fileName to catch third-party augmentations (e.g. Next.js adding `tw` to HTMLAttributes via @vercel/og).
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@code/renderers/react/src/componentManifest/reactDocgenTypescript.ts`:
- Around line 204-207: Update the stale JSDoc in reactDocgenTypescript.ts:
replace the incorrect reference to invalidateCache() with the correct function
name invalidateParser() in the top comment above the parse function so the doc
matches the actual API (search for the comment block that starts "Parse a
component file with react-docgen-typescript..." and update the function name).
- Around line 143-158: invalidateParser currently only clears the parser
variable so fileNames remains cached and the tsconfig is not re-read; update
invalidateParser to also reset fileNames (and any related cached compilerOptions
if desired) so that on the next getParser() call the condition in getParser()
(configPath && !fileNames) will be true and the tsconfig and file list (used
when building the TS program) are reloaded; modify the invalidateParser function
to set fileNames = undefined (and optionally compilerOptions = undefined)
alongside parser = undefined.
|
View your CI Pipeline Execution ↗ for commit 6d555c8
☁️ Nx Cloud last updated this comment at |
|
View your CI Pipeline Execution ↗ for commit 2329686
☁️ Nx Cloud last updated this comment at |
Package BenchmarksCommit: The following packages have significant changes to their size or dependencies:
|
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 18 | 18 | 0 |
| Self size | 1.66 MB | 1.64 MB | 🎉 -17 KB 🎉 |
| Dependency size | 9.25 MB | 9.25 MB | 🎉 -491 B 🎉 |
| Bundle Size Analyzer | Link | Link |
storybook
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 49 | 49 | 0 |
| Self size | 20.21 MB | 20.42 MB | 🚨 +205 KB 🚨 |
| Dependency size | 16.52 MB | 16.52 MB | 🎉 -2 B 🎉 |
| Bundle Size Analyzer | Link | Link |
@storybook/cli
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 183 | 183 | 0 |
| Self size | 779 KB | 779 KB | 🚨 +70 B 🚨 |
| Dependency size | 67.35 MB | 67.56 MB | 🚨 +206 KB 🚨 |
| Bundle Size Analyzer | Link | Link |
@storybook/codemod
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 176 | 176 | 0 |
| Self size | 32 KB | 32 KB | 🎉 -2 B 🎉 |
| Dependency size | 65.88 MB | 66.08 MB | 🚨 +205 KB 🚨 |
| Bundle Size Analyzer | Link | Link |
create-storybook
| Before | After | Difference | |
|---|---|---|---|
| Dependency count | 50 | 50 | 0 |
| Self size | 1.04 MB | 1.04 MB | 🚨 +974 B 🚨 |
| Dependency size | 36.74 MB | 36.94 MB | 🚨 +205 KB 🚨 |
| Bundle Size Analyzer | node | node |
The filter rejected any component whose displayName didn't start with an uppercase letter. This silently dropped all Mantine factory components (displayName like @mantine/core/Button) and potentially others. react-docgen-typescript already only returns component-like symbols, so this filter was redundant.
The system props filter only checked node_modules sources, missing CSS-in-JS libraries like Panda CSS that generate type definitions in-project (e.g. styled-system/types/style-props.d.ts with 700+ props). Expand the check to also filter .d.ts files exceeding the threshold, while preserving user-authored .ts files.
Replace the two-layer filter (isBuiltinProp + getSystemPropSources) with a single heuristic: any source file in node_modules or ending in .d.ts that contributes >30 props is filtered out entirely. This naturally catches React built-ins (HTMLAttributes, DOMAttributes), DOM interfaces (lib.dom.d.ts), and CSS-in-JS system props (Panda CSS, styled-system). Small interfaces like RefAttributes (just ref) now pass through, which is an improvement. Also removes performance instrumentation code and unused logger import.
7be464b to
88a11da
Compare
88a11da to
734f791
Compare
…config Instead of running both react-docgen and react-docgen-typescript in parallel, the manifest now reads typescript.reactDocgen from main.ts presets and runs only the selected engine. User's reactDocgenTypescriptOptions are passed through.
…pe meta - Surface react-docgen-typescript parse errors in manifest (not silently swallowed) - Remove dead esc() call in render-components-manifest - Remove unnecessary `as FileParser` cast - Add `meta` to ComponentsManifest type (required, not cast) - Simplify durationMs null checks now that meta is required Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…r optional fields)
d5b6150 to
f090172
Compare
What I did
Adds
react-docgen-typescriptas an alternative docgen engine for the component manifest system, selectable via the existingreactDocgenconfig inmain.ts. The manifest uses a single engine based on this config — eitherreact-docgen(default) orreact-docgen-typescript.previousProgram(preserved across manifest passes so TypeScript reuses unchanged source files)react-docgen-typescriptwhen using the defaultreact-docgenfindTsconfigPathto shared utilsreact-docgen-typescriptas explicit dependencyProp filtering
RDT resolves the full type tree, which includes hundreds of inherited props from React, DOM, and CSS-in-JS libraries. A single heuristic (
getLargeNonUserPropSources) filters bulk props while preserving meaningful ones:node_modulesor ending in.d.ts) contributes >30 props, all props from that source are filtered outHTMLAttributesfrom@types/react→ 200+ props), DOM interfaces (lib.dom.d.ts→ hundreds of props), and CSS-in-JS system props (Panda CSSstyle-props.d.ts→ 704 props).tsfiles are never filtered — onlynode_modulesand generated.d.tsfiles are candidatesRefAttributeswith justref) pass through since they're below the thresholdQA Reports
Tested on 5 real-world React component libraries to compare react-docgen (RD) vs react-docgen-typescript (RDT):
Why react-docgen-typescript
RDT is the clear choice for projects that need it:
The one area where RD has an edge is Primer React, where it covers 28 more components. This is likely due to specific patterns RDT doesn't match (e.g. components without explicit type annotations). Users can choose the engine that works best for their codebase.
Performance per project
ts.createProgram(one-time cost, reused across components)parseWithProgramProvider+ prop filtering per componentPark UI performance (37.9s) — root cause
Park UI uses Panda CSS, which generates massive in-project type definitions in
styled-system/types/:style-props.d.ts— 704 CSS properties inSystemProperties(7,503 lines)conditions.d.ts— 136 conditions (_hover, _focus, sm, md, lg, _dark, etc.)ConditionalValue<V>— recursive type:V | Array<V | null> | { [K in keyof Conditions]?: ConditionalValue<V> }Nested<P>— recursively self-referential:P & { [K in Selectors]?: Nested<P> } & { [K in Conditions]?: Nested<P> }Every styled component's props type is
HTMLStyledProps<T>=JsxHTMLProps<ComponentProps<T> & UnstyledProps & AsProps, JsxStyleProps>, whereJsxStyleProps = SystemStyleObject & WithCssexpands to the full 704×136 recursive type. TypeScript must resolve this for each of the 54 components, taking 500ms–2.7s per component.The bottleneck is
checker.getApparentProperties()inside react-docgen-typescript'sgetPropsInfo()— this fully resolves the type tree before any filtering can happen. RDT'spropFilteroption runs after type resolution, so it cannot short-circuit the expensive work. RDT's internalpropertiesOfPropsCacheonly caches props fromnode_modules, so Panda CSS's in-project.d.tstypes bypass the cache between components.Our system props filter correctly removes these props from the output (e.g. Button: 850 → 12 props), but the performance cost is unavoidable with the current RDT architecture.
Mantine — react-docgen (RD) vs react-docgen-typescript (RDT)
RDT: 151/183 components (82.5%) — RD: 19/183 (10.4%)
Summary
RDT extracts 23x more props and covers 8x more components than RD on this codebase.
Why RDT wins on Mantine
Mantine wraps nearly all components in
factory()orpolymorphicFactory():react-docgen (AST/Babel) pattern-matches known React patterns like
function Foo(props: FooProps)orReact.forwardRef(...). It cannot follow types throughfactory()wrappers — it just sees a function call.react-docgen-typescript uses the TypeScript compiler, so it resolves
ButtonFactory→ButtonProps→ all individual props, including inherited style/theme/polymorphic props.The only components RD successfully parses are the ones that use plain
export function:Breakdown by category
Both have props (19 components):
15 of 19 have equal prop counts. RDT finds significantly more for UnstyledButton (+61) and HoverCard (+42).
RDT-only (132 components) — top examples:
All of these use
factory()/polymorphicFactory()— completely invisible to RD.Neither (32 components): Hooks (
use-click-outside,use-move, etc.), guides, changelogs, and utilities without component props.Conclusion
For libraries using higher-order component patterns like Mantine's
factory(), RDT is essential — RD misses 132 out of 151 components entirely. Where both work, they produce identical results. RDT is the strictly better choice for Mantine.Flowbite-React — react-docgen (RD) vs react-docgen-typescript (RDT)
RDT: 42/44 components (95.5%) — RD: 42/44 (95.5%)
Summary
Both tools perform well on this codebase. RDT extracts 9% more props overall.
Setup: tsconfig paths trick
Flowbite-react's storybook lives in
apps/storybook/and imports components from theflowbite-reactpackage:By default, neither RD nor RDT could resolve the source files — both returned 0 props for all 44 components. Adding a tsconfig path alias fixed this:
This lets RDT resolve
flowbite-reactimports to the actual TypeScript source files, enabling prop extraction.Why results are similar
Flowbite-react uses simple
forwardRefpatterns that both tools handle well:No factory wrappers or complex HOC patterns — both AST and TypeScript compiler approaches work.
Breakdown by category
Both have props (41 components):
35 of 41 shared components have equal prop counts. RDT finds more for Dropdown (+10), Datepicker (+6), and Card (+3).
RDT-only (1): Pagination (13 props) — RD fails to extract props here.
RD-only (1): Button (4 props) — RDT misses this one.
Neither (1): Banner — no extractable props.
Conclusion
For libraries using standard React patterns (
forwardRef, simple function components), both tools produce near-identical results. RDT has a slight edge with 308 vs 282 total props and uniquely resolves Pagination. The tsconfigpathstrick is essential for monorepos where stories import from a package name rather than relative paths.Primer React — react-docgen (RD) vs react-docgen-typescript (RDT)
RDT: 161/225 components (71.6%) — RD: 189/225 (84%)
Summary
Both tools perform well. RD covers more components (189 vs 161), while RDT extracts slightly more total props (1,061 vs 1,016).
Where both have props (150 components)
RDT wins big on: SelectPanel (RD=18, RDT=47), AnchoredOverlay (RD=15, RDT=22), Octicon (RD=2, RDT=7)
RD-only (39 entries, 21 unique components): Textarea, TextInputWithTokens, FormControl, Radio, Checkbox, Overlay, Label, Flash, SideNav, etc.
RDT-only (11 entries, 5 unique components): RelativeTime (26 props), Box, Portal, SegmentedControlIconButton, UnderlinePanels
Conclusion
Primer uses standard React patterns that both tools handle well. This is the most balanced result — RD has broader coverage (189 vs 161 components) while RDT extracts deeper props where it works (1,061 vs 1,016 total). The tools are complementary here.
Park UI — react-docgen (RD) vs react-docgen-typescript (RDT)
RDT: 25/54 components (46.3%) — RD: 8/54 (14.8%)
Summary
RDT covers 3x more components and extracts 36x more props than RD.
Park UI is built on Panda CSS, which generates system props in in-project
.d.tsfiles (styled-system/types/style-props.d.tswith 703SystemProperties,types/conditions.d.tswith 135Conditions). The.d.tssystem props filter correctly removes these bulk props while preserving meaningful component-specific props.Both have props (8 components)
RDT consistently finds 10-30x more props per component.
RDT-only (17 components) — top examples
Conclusion
RDT is the clear winner for Park UI. RD barely extracts any props (1-2 per component), while RDT provides meaningful prop coverage. The
.d.tssystem props filter is essential here — without it, each component would show 800+ Panda CSS system props.Reshaped — react-docgen (RD) vs react-docgen-typescript (RDT)
RDT: 63/78 components (80.8%) — RD: 0/78 (0%)
Summary
RDT is the only tool that works on this codebase. RD extracts zero props.
Why RD fails completely
Reshaped uses
export defaultwith types imported from a separate file:react-docgen cannot follow the
type * as Tnamespace import to resolveT.Props. react-docgen-typescript resolves this through the TypeScript compiler.RDT-only (63 components) — top examples
Neither (15 components): Hooks (
useHotkeys,useToggle, etc.) and utility components without props.Conclusion
Reshaped demonstrates a pattern where RDT is the only viable option. The namespace type import pattern (
type * as T) is completely opaque to RD's AST-based analysis.Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
Caution
This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!
Tested on 5 real-world React projects (Mantine, Flowbite-React, Primer React, Park UI, Reshaped) by publishing canary releases and building Storybook with
experimentalComponentsManifestenabled. Verified prop extraction, system prop filtering, and compared RD vs RDT results. See QA Reports above for detailed results.Documentation
MIGRATION.MD
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal,ci:mergedorci:dailyGH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.tsMake sure this PR contains one of the labels below:
Available labels
bug: Internal changes that fixes incorrect behavior.maintenance: User-facing maintenance tasks.dependencies: Upgrading (sometimes downgrading) dependencies.build: Internal-facing build tooling & test updates. Will not show up in release changelog.cleanup: Minor cleanup style change. Will not show up in release changelog.documentation: Documentation only changes. Will not show up in release changelog.feature request: Introducing a new feature.BREAKING CHANGE: Changes that break compatibility in some way with current major version.other: Changes that don't fit in the above categories.🦋 Canary release
This pull request has been released as version
0.0.0-pr-33818-sha-b6e1582e. Try it out in a new sandbox by runningnpx storybook@0.0.0-pr-33818-sha-b6e1582e sandboxor in an existing project withnpx storybook@0.0.0-pr-33818-sha-b6e1582e upgrade.More information
0.0.0-pr-33818-sha-b6e1582ekasper/react-docgen-typescriptb6e1582e1770970035)To request a new release of this pull request, mention the
@storybookjs/coreteam.core team members can create a new canary release here or locally with
gh workflow run --repo storybookjs/storybook publish.yml --field pr=33818