Skip to content

Conversation

@yannbf
Copy link
Member

@yannbf yannbf commented Jan 19, 2026

What I did

This reintroduces support for react-docgen-typescript and its options defined in main.ts, which were supported before and therefore this fixes that issue.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

  1. Define react-docgen-typescript usage in main.ts like so:
const config = defineMain({
  typescript: {
    reactDocgen: 'react-docgen-typescript',
  },
});

export default config;
  1. Run Storybook and create a story for a typescript heavy component like
// ERROR: React docgen doesn't identify anything from MediumTitle
import { ComponentProps, FC, PropsWithChildren } from 'react';

type Sizes = 'large' | 'medium' | 'small';

const Title: FC<PropsWithChildren<{ color: string; size?: Sizes }>> = ({
  color,
  size = 'large',
  children,
}) => (
  <div>
    {children}
  </div>
);

export const MediumTitle: FC<Omit<ComponentProps<typeof Title>, 'size'>> = (props) => (
  <Title size="medium" {...props} />
);
  1. See args being created correctly
  2. Switch back to react-docgen in main.ts
  3. Delete the previously generated story file and do the same flow for the same component
  4. See that the flow is faster but less precise on args

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update
    MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make 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-33586-sha-bb7f266c. Try it out in a new sandbox by running npx storybook@0.0.0-pr-33586-sha-bb7f266c sandbox or in an existing project with npx storybook@0.0.0-pr-33586-sha-bb7f266c upgrade.

More information
Published version 0.0.0-pr-33586-sha-bb7f266c
Triggered by @yannbf
Repository storybookjs/storybook
Branch yann/story-creation-docgen-ts
Commit bb7f266c
Datetime Mon Jan 19 16:19:09 UTC 2026 (1768839549)
Workflow run 21144553127

To request a new release of this pull request, mention the @storybookjs/core team.

core team members can create a new canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=33586

Summary by CodeRabbit

Release Notes

  • Refactor

    • Reorganized component documentation extraction infrastructure with improved modularity and shared utilities.
  • New Features

    • Added support for selecting between multiple documentation analysis tools based on project configuration.
    • Enhanced component property type detection and mapping capabilities.

✏️ Tip: You can customize this high-level summary in your review settings.

@yannbf yannbf self-assigned this Jan 19, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

This PR reorganizes React DocGen extraction utilities by migrating from a single extractor module to a multi-extractor architecture. It introduces support for both react-docgen and react-docgen-typescript tools, consolidates shared types and helpers into a utils module, and updates the preset system to route between extractors based on configuration.

Changes

Cohort / File(s) Summary
DocGen Extraction Refactoring
code/renderers/react/src/componentManifest/reactDocgen/extractDocgenInfo.ts
Deleted file containing original React DocGen extraction utilities, including type guards, type definitions, and mapping functions.
React DocGen Extractor
code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts
New module providing mapReactDocgenToArgType, getComponentDocgen, and extractArgTypesFromDocgen to convert react-docgen types to SBType with support for unions, arrays, tuples, literals, and object signatures.
React DocGen TypeScript Extractor
code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts
New module providing mapReactDocgenTypescriptToArgType and extractArgTypesFromDocgenTypescript to parse and map react-docgen-typescript string types to SBType with async parsing and error handling.
Shared DocGen Utilities
code/renderers/react/src/componentManifest/reactDocgen/utils.ts
New module centralizing shared types (ReactDocgenTsType, ReactDocgenElementsType, etc.), type guards (isElementsType, isObjectSignatureType, isLiteralType), and common mapping logic (mapCommonTypes, REACT_NODE_TYPES, KNOWN_TYPE_NAMES).
Test Updates
code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts
Updated import paths and test structure to reference extractReactDocgenInfo module; added describe wrapper for better test organization.
Preset Integration
code/renderers/react/src/preset.ts
Modified internal_getArgTypesData to accept presets configuration and route extraction calls to either extractArgTypesFromDocgen or extractArgTypesFromDocgenTypescript based on preset settings.

Sequence Diagram

sequenceDiagram
    participant Client as Preset Handler
    participant Router as internal_getArgTypesData
    participant Extractor1 as extractArgTypesFromDocgen
    participant Extractor2 as extractArgTypesFromDocgenTypescript
    participant Mapper1 as mapReactDocgenToArgType
    participant Mapper2 as mapReactDocgenTypescriptToArgType
    participant Utils as utils (mapCommonTypes)

    Client->>Router: internal_getArgTypesData(options, presets)
    activate Router
    Router->>Router: Check preset reactDocgen value
    
    alt reactDocgen === 'react-docgen-typescript'
        Router->>Extractor2: extractArgTypesFromDocgenTypescript()
        activate Extractor2
        Extractor2->>Extractor2: Load react-docgen-typescript parser
        Extractor2->>Extractor2: Parse component file
        loop For each prop
            Extractor2->>Mapper2: mapReactDocgenTypescriptToArgType(typeString)
            activate Mapper2
            Mapper2->>Utils: mapCommonTypes(typeName)
            Utils-->>Mapper2: SBType | null
            Mapper2-->>Extractor2: SBType
            deactivate Mapper2
        end
        Extractor2-->>Router: ComponentArgTypesData | null
        deactivate Extractor2
    else reactDocgen !== false
        Router->>Extractor1: extractArgTypesFromDocgen()
        activate Extractor1
        Extractor1->>Extractor1: Parse component file with react-docgen
        loop For each prop
            Extractor1->>Mapper1: mapReactDocgenToArgType(docgenType)
            activate Mapper1
            Mapper1->>Utils: mapCommonTypes(typeName)
            Utils-->>Mapper1: SBType | null
            Mapper1-->>Extractor1: SBType
            deactivate Mapper1
        end
        Extractor1-->>Router: ComponentArgTypesData | null
        deactivate Extractor1
    end
    
    Router-->>Client: ComponentArgTypesData | null
    deactivate Router
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.test.ts`:
- Around line 188-191: The vi.mock call that stubs 'react-docgen-typescript' is
currently inside a test and must be moved to the top of the test file so it is
hoisted correctly; change the module mock invocation to
vi.mock('react-docgen-typescript', { spy: true, }, ) at file top (outside tests)
and remove the in-test vi.mock; then configure mock behavior (e.g. make
withCompilerOptions return mockParser) inside a beforeEach block by setting the
mock implementation/returnValue on the mocked withCompilerOptions function
(referencing withCompilerOptions and mockParser) so each test gets a fresh,
configured mock.

In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts`:
- Around line 63-78: The union-handling logic in
extractReactTypescriptDocgenInfo that splits on ' | ' can break for nested or
generics (e.g., "((a: string) => void) | null" or "Map<string, A | B>"); update
the union parsing block (the if (trimmed.includes(' | ')) branch) to add a clear
inline comment documenting this limitation and examples, and mention a
recommended future improvement (parse with the TypeScript AST or a proper
tokenizer) so future maintainers understand why splitting was chosen and the
known edge cases.
🧹 Nitpick comments (5)
code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts (1)

137-162: Consider adding explicit return type annotation for clarity.

The function's return type is inferred, but given it's part of the public API, an explicit return type would improve maintainability and documentation.

Suggested improvement
-export const extractArgTypesFromDocgen = ({
+export const extractArgTypesFromDocgen = ({
   componentFilePath,
   componentExportName,
-}: GetArgTypesDataOptions) => {
+}: GetArgTypesDataOptions): ComponentArgTypesData | null => {
code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts (1)

43-45: Track the @ts-expect-error comments for future cleanup.

Multiple test assertions use @ts-expect-error with "fix this later" comments. This suggests the return type structure from getComponentDocgen could be refined to avoid these type errors.

Would you like me to open an issue to track cleaning up these type assertions, or help refine the return type of getComponentDocgen to eliminate the need for @ts-expect-error?

Also applies to: 76-78, 109-115, 194-196, 294-296

code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.test.ts (1)

12-15: Move beforeEach inside the describe blocks for better isolation.

Per coding guidelines, mock behaviors should be in beforeEach blocks, and related mocks should be grouped together. The current module-level beforeEach applies to all tests, but the filesystem setup is only needed for extractArgTypesFromDocgenTypescript tests, not the pure function unit tests.

Suggested improvement
-beforeEach(() => {
-  vi.spyOn(process, 'cwd').mockReturnValue('/app');
-  vol.fromJSON(fsMocks, '/app');
-});
-
 describe('mapReactDocgenTypescriptToArgType', () => {
   test('maps primitive types correctly', () => {

Move the beforeEach into the extractArgTypesFromDocgenTypescript describe block where it's actually needed:

 describe('extractArgTypesFromDocgenTypescript', () => {
+  beforeEach(() => {
+    vi.spyOn(process, 'cwd').mockReturnValue('/app');
+    vol.fromJSON(fsMocks, '/app');
+  });
+
   test('extracts arg types from a simple component', async () => {
code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts (2)

88-91: Potential false positive: numeric string detection.

!isNaN(Number(trimmed)) returns true for empty strings and whitespace (since Number('') is 0 and Number(' ') is 0). While the input is trimmed, if trimmed were empty, this would incorrectly return { name: 'literal', value: 0 }.

Given the earlier trimmed = typeString.trim() and the various checks before this point, an empty string is unlikely to reach here, but adding a guard would be defensive:

Suggested improvement
   // Handle literal number values
-  if (!isNaN(Number(trimmed))) {
+  if (trimmed !== '' && !isNaN(Number(trimmed))) {
     return { name: 'literal', value: Number(trimmed) };
   }

148-150: Component matching by filePath could produce unexpected results.

The fallback d.filePath?.includes(componentExportName) matches if the component name appears anywhere in the file path, which could match unintended components (e.g., searching for "Button" might match "/components/ButtonGroup/ButtonGroup.tsx").

Consider using a more precise match:

Suggested improvement
       const match = docgens.find(
-        (d) => d.displayName === componentExportName || d.filePath?.includes(componentExportName)
+        (d) => d.displayName === componentExportName
       );

The displayName match should be sufficient since react-docgen-typescript populates it for exported components. If filePath matching is truly needed, consider matching just the filename portion.

Comment on lines 188 to 191
vi.mock('react-docgen-typescript', () => ({
withCompilerOptions: vi.fn().mockReturnValue(mockParser),
}));

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Mock placement issue: vi.mock is called inside a test case.

Per coding guidelines, mocks should be placed at the top of the test file before any test cases, not inside individual tests. Additionally, vi.mock is hoisted by Vitest, so calling it inside a test doesn't work as expected - the mock applies to the entire module.

Suggested fix

Move the mock to the top of the file and configure behavior in beforeEach:

+vi.mock('react-docgen-typescript', { spy: true });
+
+// At module top, after imports
+
 describe('extractArgTypesFromDocgenTypescript', () => {
+  beforeEach(() => {
+    vi.spyOn(process, 'cwd').mockReturnValue('/app');
+    vol.fromJSON(fsMocks, '/app');
+    
+    // Configure mock behavior here
+  });
+
   test('extracts arg types from a simple component', async () => {
-    // ...
-    vi.mock('react-docgen-typescript', () => ({
-      withCompilerOptions: vi.fn().mockReturnValue(mockParser),
-    }));

As per coding guidelines, mocks should use vi.mock() with the spy: true option and mock implementations should be in beforeEach blocks.

🤖 Prompt for AI Agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.test.ts`
around lines 188 - 191, The vi.mock call that stubs 'react-docgen-typescript' is
currently inside a test and must be moved to the top of the test file so it is
hoisted correctly; change the module mock invocation to
vi.mock('react-docgen-typescript', { spy: true, }, ) at file top (outside tests)
and remove the in-test vi.mock; then configure mock behavior (e.g. make
withCompilerOptions return mockParser) inside a beforeEach block by setting the
mock implementation/returnValue on the mocked withCompilerOptions function
(referencing withCompilerOptions and mockParser) so each test gets a fresh,
configured mock.
Comment on lines +63 to +78
// Handle union types: Type1 | Type2
if (trimmed.includes(' | ')) {
const parts = trimmed.split(' | ').map((p) => p.trim());
// Check if it's a literal union (all parts are string literals)
const allLiterals = parts.every((p) => p.startsWith('"') || p.startsWith("'"));
if (allLiterals) {
return {
name: 'union',
value: parts.map((p) => ({
name: 'literal' as const,
value: p.replace(/^["']|["']$/g, ''),
})),
};
}
return { name: 'union', value: parts.map(mapReactDocgenTypescriptToArgType) };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Union parsing with | split may fail for complex nested types.

Splitting on | works for simple unions but could incorrectly split types like ((a: string) => void) | null or generic types with unions inside angle brackets like Map<string, A | B>.

For the current use case with react-docgen-typescript output, this is likely acceptable since complex nested unions are typically represented differently. However, be aware this is a known limitation.

Consider adding a comment to document this limitation:

   // Handle union types: Type1 | Type2
+  // Note: Simple split on ' | ' - may not correctly handle deeply nested unions
   if (trimmed.includes(' | ')) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle union types: Type1 | Type2
if (trimmed.includes(' | ')) {
const parts = trimmed.split(' | ').map((p) => p.trim());
// Check if it's a literal union (all parts are string literals)
const allLiterals = parts.every((p) => p.startsWith('"') || p.startsWith("'"));
if (allLiterals) {
return {
name: 'union',
value: parts.map((p) => ({
name: 'literal' as const,
value: p.replace(/^["']|["']$/g, ''),
})),
};
}
return { name: 'union', value: parts.map(mapReactDocgenTypescriptToArgType) };
}
// Handle union types: Type1 | Type2
// Note: Simple split on ' | ' - may not correctly handle deeply nested unions
if (trimmed.includes(' | ')) {
const parts = trimmed.split(' | ').map((p) => p.trim());
// Check if it's a literal union (all parts are string literals)
const allLiterals = parts.every((p) => p.startsWith('"') || p.startsWith("'"));
if (allLiterals) {
return {
name: 'union',
value: parts.map((p) => ({
name: 'literal' as const,
value: p.replace(/^["']|["']$/g, ''),
})),
};
}
return { name: 'union', value: parts.map(mapReactDocgenTypescriptToArgType) };
}
🤖 Prompt for AI Agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts`
around lines 63 - 78, The union-handling logic in
extractReactTypescriptDocgenInfo that splits on ' | ' can break for nested or
generics (e.g., "((a: string) => void) | null" or "Map<string, A | B>"); update
the union parsing block (the if (trimmed.includes(' | ')) branch) to add a clear
inline comment documenting this limitation and examples, and mention a
recommended future improvement (parse with the TypeScript AST or a proper
tokenizer) so future maintainers understand why splitting was chosen and the
known edge cases.
@yannbf yannbf added the ci:merged Run the CI jobs that normally run when merged. label Jan 19, 2026
@nx-cloud
Copy link

nx-cloud bot commented Jan 19, 2026

View your CI Pipeline Execution ↗ for commit bb7f266

Command Status Duration Result
nx run-many -t compile,check,knip,test,pretty-d... ❌ Failed 11m 20s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-19 16:30:50 UTC

@yannbf yannbf force-pushed the yann/story-creation-docgen-ts branch from a1daad6 to d2323bb Compare January 19, 2026 16:12
@ndelangen ndelangen changed the title Core: Fix react-docgen-typescript support in story creation Jan 19, 2026
Comment on lines +134 to +137
const parser = withCompilerOptions(
{ esModuleInterop: true, jsx: 2 /* React */ },
mergedOptions
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might look into sharing this over multiple invocations of extractArgTypesFromDocgenTypescript since currently the typescript runtime Program gets re-instantiated every call.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But likely, performance isn't the highest priority right now?

@storybook-app-bot
Copy link

Package Benchmarks

Commit: bb7f266, ran on 19 January 2026 at 16:28:07 UTC

The following packages have significant changes to their size or dependencies:

@storybook/nextjs

Before After Difference
Dependency count 538 538 0
Self size 646 KB 646 KB 🎉 -130 B 🎉
Dependency size 59.22 MB 59.28 MB 🚨 +58 KB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs-vite

Before After Difference
Dependency count 127 127 0
Self size 1.12 MB 1.12 MB 🎉 -22 B 🎉
Dependency size 21.82 MB 21.87 MB 🚨 +49 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-native-web-vite

Before After Difference
Dependency count 159 159 0
Self size 30 KB 30 KB 🎉 -10 B 🎉
Dependency size 23.00 MB 23.17 MB 🚨 +164 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-vite

Before After Difference
Dependency count 117 117 0
Self size 35 KB 35 KB 🎉 -44 B 🎉
Dependency size 19.62 MB 19.66 MB 🚨 +49 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-webpack5

Before After Difference
Dependency count 278 278 0
Self size 24 KB 24 KB 🚨 +2 B 🚨
Dependency size 44.13 MB 44.19 MB 🚨 +58 KB 🚨
Bundle Size Analyzer Link Link

@storybook/cli

Before After Difference
Dependency count 183 183 0
Self size 775 KB 775 KB 🚨 +139 B 🚨
Dependency size 67.38 MB 67.46 MB 🚨 +79 KB 🚨
Bundle Size Analyzer Link Link

@storybook/codemod

Before After Difference
Dependency count 176 176 0
Self size 30 KB 30 KB 🎉 -4 B 🎉
Dependency size 65.95 MB 66.03 MB 🚨 +79 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react

Before After Difference
Dependency count 57 57 0
Self size 732 KB 781 KB 🚨 +49 KB 🚨
Dependency size 12.94 MB 12.94 MB 🚨 +409 B 🚨
Bundle Size Analyzer Link Link
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts`:
- Around line 150-158: The loop in extractReactDocgenInfo.ts currently ignores
props that lack tsType; update the logic in the for-loop that iterates over
props (where tsType is derived from (propInfo as
ReactDocgenPropDescriptor).tsType) to fall back to (propInfo as
ReactDocgenPropDescriptor).type when tsType is undefined, and pass that fallback
into mapReactDocgenToArgType so props defined with PropTypes/type are preserved;
keep setting required via (propInfo as ReactDocgenPropDescriptor).required and
assign the result into mapped.props![propName]. Ensure mapReactDocgenToArgType
can accept the docgen "type" shape or normalize it before mapping.

In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts`:
- Around line 145-155: The current matching logic in
extractReactTypescriptDocgenInfo.ts uses filePath.includes(componentExportName)
which can produce false positives (e.g., "Button" matching "IconButton"); update
the matching to only consider component identifiers like displayName,
actualName, or exportName from each docgen entry: change the predicate in
docgens.find(...) to (d) => d.displayName === componentExportName ||
d.actualName === componentExportName || d.exportName === componentExportName,
removing the filePath.includes check; if you must keep file path matching,
replace it with a strict basename match (compare the file's basename without
extension to componentExportName) rather than substring includes.

In `@code/renderers/react/src/preset.ts`:
- Around line 91-114: The code ignores explicit
GetArgTypesDataOptions.reactDocgen and .reactDocgenTypescriptOptions because it
only reads them from presets; update the logic in the function that uses
InternalGetArgTypesDataOptions so that you first collect typescriptOptions from
presets.apply('typescript', {}) (as now) but then merge the explicit options
from the incoming options object into that result with the incoming options
taking priority (i.e., override preset values with options.reactDocgen and
options.reactDocgenTypescriptOptions if they are provided), and ensure the
merged values are then used for the reactDocgen / reactDocgenTypescriptOptions
checks (preserve current fallback to defaults and the reactDocgen === false
early return).
🧹 Nitpick comments (2)
code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts (2)

1-13: Reset mocks/volume after each test to avoid leakage.

The cwd spy and memfs volume can persist across suites if not restored.

♻️ Suggested cleanup
-import { beforeEach, describe, expect, test, vi } from 'vitest';
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
@@
   beforeEach(() => {
     vi.spyOn(process, 'cwd').mockReturnValue('/app');
     vol.fromJSON(fsMocks, '/app');
   });
+  afterEach(() => {
+    vi.restoreAllMocks();
+    vol.reset();
+  });

40-45: Replace @ts-expect-error with a typed success helper.

You already assert success; a small helper can narrow the type and avoid suppressions across the suite.

💡 Example helper
const assertSuccess = (result: ReturnType<typeof getComponentDocgen>) => {
  if (!result || result.reactDocgen.type !== 'success') {
    throw new Error('Expected react-docgen success result');
  }
  return result.reactDocgen.data;
};

// usage
const data = assertSuccess(result);
expect(data.displayName).toBe('Button');
Comment on lines +150 to +158
for (const [propName, propInfo] of Object.entries(props)) {
const tsType = (propInfo as ReactDocgenPropDescriptor).tsType as ReactDocgenTsType | undefined;
if (!tsType) {
continue;
}
mapped.props![propName] = {
required: Boolean((propInfo as ReactDocgenPropDescriptor).required),
type: mapReactDocgenToArgType(tsType),
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts | head -180 | tail -40

Repository: storybookjs/storybook

Length of output: 1534


🏁 Script executed:

rg "ReactDocgenPropDescriptor|ReactDocgenTsType" code/renderers/react --type ts -A 3 -B 1

Repository: storybookjs/storybook

Length of output: 4975


🏁 Script executed:

rg "mapReactDocgenToArgType" code/renderers/react --type ts -A 10 -B 2

Repository: storybookjs/storybook

Length of output: 7471


🏁 Script executed:

rg "react-docgen" code/renderers/react --type ts --type json -A 2 -B 1 | head -100

Repository: storybookjs/storybook

Length of output: 9505


🏁 Script executed:

rg "type PropDescriptor" --type ts -A 10 code/renderers/react

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts | head -100

Repository: storybookjs/storybook

Length of output: 3555


🏁 Script executed:

rg "propInfo.*\.type|\.type\s*as" code/renderers/react --type ts -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 997


🏁 Script executed:

python3 << 'EOF'
import subprocess
import json

# Look for node_modules or check package.json to understand what fields are in PropDescriptor
result = subprocess.run(['find', 'code/renderers/react/node_modules', '-name', 'index.d.ts', '-path', '*react-docgen*'], 
                       capture_output=True, text=True, timeout=10)
print("Found type files:")
print(result.stdout)
EOF

Repository: storybookjs/storybook

Length of output: 151


🏁 Script executed:

rg "flowType|\.type" code/renderers/react/src/componentManifest/reactDocgen --type ts -B 1 -A 1 | head -50

Repository: storybookjs/storybook

Length of output: 3116


🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts | head -80

Repository: storybookjs/storybook

Length of output: 3290


🏁 Script executed:

rg "PropDescriptor" code/renderers/react/src --type ts -B 2 -A 5

Repository: storybookjs/storybook

Length of output: 2456


🏁 Script executed:

rg "tsType|flowType" code/renderers/react --type ts -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 24008


🏁 Script executed:

rg "propInfo\.type\b|propInfo\['type'\]" code/renderers/react --type ts -B 3 -A 3

Repository: storybookjs/storybook

Length of output: 1390


🏁 Script executed:

rg "PropDescriptor|type.*flowType" --type ts -B 3 -A 3 code/renderers/react | head -80

Repository: storybookjs/storybook

Length of output: 2345


🏁 Script executed:

rg "PropTypes|\.js" code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts -B 3 -A 3

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

rg "handler|resolver|tsType|flowType" code/renderers/react/src/componentManifest/reactDocgen.ts -B 2 -A 2 | head -100

Repository: storybookjs/storybook

Length of output: 644


🏁 Script executed:

rg "propInfo\[" code/renderers/react --type ts -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

python3 << 'EOF'
import json
import subprocess

# Try to find react-docgen package and check its types
result = subprocess.run(
    ['find', 'code/renderers/react', '-type', 'f', '-name', 'package.json'],
    capture_output=True, text=True
)

for pkg_json in result.stdout.strip().split('\n')[:3]:
    if pkg_json:
        print(f"\n=== {pkg_json} ===")
        result = subprocess.run(['head', '-30', pkg_json], capture_output=True, text=True)
        print(result.stdout)
EOF

Repository: storybookjs/storybook

Length of output: 982


🏁 Script executed:

rg "description|deprecation|flowType|tsType" code/renderers/react/src/componentManifest/reactDocgen --type ts -B 2 -A 1 | head -60

Repository: storybookjs/storybook

Length of output: 5790


🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts | tail -150

Repository: storybookjs/storybook

Length of output: 4636


🌐 Web query:

react-docgen v8 PropDescriptor tsType type fields structure

💡 Result:

Short answer — in react-docgen v8 a prop's tsType is a TypeDescriptor, which is a tagged union. The possible shapes and their important fields are:

  • TSFunctionSignatureType (function signature)

    • signature: { arguments: Array, return?: TypeDescriptor, this?: TypeDescriptor }.
    • each FunctionArgumentType: { name: string; rest?: boolean; type?: TypeDescriptor }.[1]
  • ObjectSignatureType (object / interface)

    • properties: Array<{ key: TypeDescriptor | string; value: TypeDescriptor; description?: string }>.
    • constructor?: TypeDescriptor.[1]
  • ElementsType (array/tuple of element types)

    • elements: Array; name: 'elements' (type kind).[1]
  • LiteralType

    • raw, value (literal details) — represents literal types (e.g. "foo", 1). [1][2]
  • SimpleType

    • name: string; raw?: string — basic named types (e.g. 'string', 'number', 'union', etc.).[1][2]

Notes:

  • TypeDescriptor is defined as ElementsType | LiteralType | ObjectSignatureType | SimpleType | T (so a tsType may itself be a TS function-signature node).
  • Use the getTSType / getTypeAnnotation utilities to see how these are produced. [1][2]

Sources: react-docgen v8 type definitions. [1] API/index (dist .d.ts) and [2] package main type exports.


🏁 Script executed:

rg "\.jsx?|PropTypes" code/renderers/react/src/componentManifest/reactDocgen --type ts -B 3 -A 3 | head -80

Repository: storybookjs/storybook

Length of output: 1812


🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts | head -80

Repository: storybookjs/storybook

Length of output: 3231


🏁 Script executed:

rg "flowType|description|deprecated" code/renderers/react --type ts -B 2 -A 2 | head -60

Repository: storybookjs/storybook

Length of output: 4544


🏁 Script executed:

python3 << 'EOF'
# Search for any files that mention what fields PropDescriptor has
import subprocess

# Check if there are any .d.ts files or type stubs that describe PropDescriptor
result = subprocess.run(
    ['find', 'code/renderers/react', '-type', 'f', '-name', '*.d.ts'],
    capture_output=True, text=True, timeout=10
)

files = [f for f in result.stdout.strip().split('\n') if f and 'react-docgen' in f]
for f in files[:5]:
    print(f"=== {f} ===")
    result = subprocess.run(['head', '-100', f], capture_output=True, text=True)
    print(result.stdout)
    print()
EOF

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

rg "tsType|type.*fallback" code/renderers/react/src/componentManifest/reactDocgen -B 3 -A 3

Repository: storybookjs/storybook

Length of output: 21891


🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts | sed -n '19,76p'

Repository: storybookjs/storybook

Length of output: 2420


🏁 Script executed:

python3 << 'EOF'
import subprocess

# Search for actual usage patterns of propInfo fields
result = subprocess.run(
    ['rg', 'propInfo\\s*\\.', 'code/renderers/react/src/componentManifest', '--type', 'ts', '-o'],
    capture_output=True, text=True, timeout=10
)

# Get unique field accesses
fields = set()
for line in result.stdout.strip().split('\n'):
    if line:
        # Extract field name
        parts = line.split('propInfo.')
        if len(parts) > 1:
            field = parts[1].split('?')[0].split('!')[0].split('[')[0].split(' ')[0].split('(')[0]
            if field:
                fields.add(field)

print("Fields accessed on propInfo:")
for f in sorted(fields):
    print(f"  - {f}")
EOF

Repository: storybookjs/storybook

Length of output: 93


🏁 Script executed:

rg "propInfo\[|propInfo\." code/renderers/react/src/componentManifest --type ts -B 1 -A 1

Repository: storybookjs/storybook

Length of output: 889


Add fallback to support props with only the type field.

The PropDescriptor has both tsType and type fields. The current code only checks tsType, causing props with only a type field to be dropped. This can happen with JavaScript/PropTypes components. Add a fallback to the type field:

🔧 Suggested fix
   for (const [propName, propInfo] of Object.entries(props)) {
     const tsType = (propInfo as ReactDocgenPropDescriptor).tsType as ReactDocgenTsType | undefined;
-    if (!tsType) {
+    const fallbackType = (propInfo as ReactDocgenPropDescriptor).type as ReactDocgenTsType | undefined;
+    const docgenType = tsType ?? fallbackType;
+    if (!docgenType) {
       continue;
     }
     mapped.props![propName] = {
       required: Boolean((propInfo as ReactDocgenPropDescriptor).required),
-      type: mapReactDocgenToArgType(tsType),
+      type: mapReactDocgenToArgType(docgenType),
     };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const [propName, propInfo] of Object.entries(props)) {
const tsType = (propInfo as ReactDocgenPropDescriptor).tsType as ReactDocgenTsType | undefined;
if (!tsType) {
continue;
}
mapped.props![propName] = {
required: Boolean((propInfo as ReactDocgenPropDescriptor).required),
type: mapReactDocgenToArgType(tsType),
};
for (const [propName, propInfo] of Object.entries(props)) {
const tsType = (propInfo as ReactDocgenPropDescriptor).tsType as ReactDocgenTsType | undefined;
const fallbackType = (propInfo as ReactDocgenPropDescriptor).type as ReactDocgenTsType | undefined;
const docgenType = tsType ?? fallbackType;
if (!docgenType) {
continue;
}
mapped.props![propName] = {
required: Boolean((propInfo as ReactDocgenPropDescriptor).required),
type: mapReactDocgenToArgType(docgenType),
};
🤖 Prompt for AI Agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts`
around lines 150 - 158, The loop in extractReactDocgenInfo.ts currently ignores
props that lack tsType; update the logic in the for-loop that iterates over
props (where tsType is derived from (propInfo as
ReactDocgenPropDescriptor).tsType) to fall back to (propInfo as
ReactDocgenPropDescriptor).type when tsType is undefined, and pass that fallback
into mapReactDocgenToArgType so props defined with PropTypes/type are preserved;
keep setting required via (propInfo as ReactDocgenPropDescriptor).required and
assign the result into mapped.props![propName]. Ensure mapReactDocgenToArgType
can accept the docgen "type" shape or normalize it before mapping.
Comment on lines +145 to +155
// Find the matching component if a name is specified
let targetDocgen = docgens[0];
if (componentExportName) {
const match = docgens.find(
(d) => d.displayName === componentExportName || d.filePath?.includes(componentExportName)
);
if (match) {
targetDocgen = match;
} else {
// If specific component requested but not found, return null
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts | head -160

Repository: storybookjs/storybook

Length of output: 6483


🏁 Script executed:

# Check the overall file structure
wc -l code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts

Repository: storybookjs/storybook

Length of output: 159


🏁 Script executed:

# Search for usages of this function to understand how it's called
rg "extractReactTypescriptDocgenInfo|componentExportName" code/renderers/react/src/componentManifest/ -A 2 -B 2

Repository: storybookjs/storybook

Length of output: 2958


🏁 Script executed:

# Look for test files related to this module
fd "extractReactTypescriptDocgenInfo" code/ --type f

Repository: storybookjs/storybook

Length of output: 155


🏁 Script executed:

# Check if there are any components with similar naming patterns (Button, IconButton, etc.)
fd -e tsx -e ts "Button" code/renderers/react/src --max-one-result | head -20

Repository: storybookjs/storybook

Length of output: 298


🏁 Script executed:

# Find test files related to reactDocgen
fd "test|spec" code/renderers/react/src/componentManifest/reactDocgen/ --type f

Repository: storybookjs/storybook

Length of output: 150


🏁 Script executed:

# Search for tests in parent directory
fd "extractReactTypescriptDocgenInfo" code/ --type f -path "*test*" -o -path "*spec*"

Repository: storybookjs/storybook

Length of output: 233


🏁 Script executed:

# Look for tests in the entire react renderer
find code/renderers/react -name "*.test.ts*" -o -name "*.spec.ts*" | grep -i "docgen"

Repository: storybookjs/storybook

Length of output: 213


🏁 Script executed:

# Search for where this function is called
rg "extractArgTypesFromDocgenTypescript" code/renderers/react --type ts --type tsx -B 3 -A 3

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

# Check react-docgen-typescript documentation behavior - look for examples in the codebase
rg "filePath" code/renderers/react/src/componentManifest/reactDocgen/ -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 3618


🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen.test.ts | head -100

Repository: storybookjs/storybook

Length of output: 3483


🏁 Script executed:

cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts | head -150

Repository: storybookjs/storybook

Length of output: 5423


🏁 Script executed:

# Look for where filePath comes from - check react-docgen-typescript output
rg "parser\.parse" code/renderers/react --type f -A 5 -B 5 | head -50

Repository: storybookjs/storybook

Length of output: 90


🏁 Script executed:

# Check if there are any Button/IconButton components in the repo to understand real-world scenario
find code -name "Button.tsx" -o -name "IconButton.tsx" | head -20

Repository: storybookjs/storybook

Length of output: 692


🏁 Script executed:

find code -name "IconButton.tsx"

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Look for how the function is actually called - check for real usages
rg "extractArgTypesFromDocgenTypescript" code --type file -B 5 -A 5

Repository: storybookjs/storybook

Length of output: 93


🏁 Script executed:

# Check the other similar function that also uses filePath matching
cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts | grep -A 20 "getComponentDocgen"

Repository: storybookjs/storybook

Length of output: 1671


🏁 Script executed:

# Look specifically at the implementation details of how displayName and filePath are used
rg "displayName.*filePath|filePath.*displayName" code/renderers/react --type file -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 93


🏁 Script executed:

# Search for where extractArgTypesFromDocgenTypescript is actually called
rg "extractArgTypesFromDocgenTypescript" code/renderers/react -A 10 -B 5

Repository: storybookjs/storybook

Length of output: 4542


🏁 Script executed:

# Look at componentManifest.ts to see how these functions are called
cat -n code/renderers/react/src/componentManifest/reactDocgen.ts | head -100

Repository: storybookjs/storybook

Length of output: 3874


🏁 Script executed:

# Check what react-docgen-typescript actually returns for filePath
rg "react-docgen-typescript" code/renderers/react --max-count 5

Repository: storybookjs/storybook

Length of output: 1504


🏁 Script executed:

# Let's examine the actual file paths in practice - check examples in the code
rg "filePath.*includes|includes.*filePath" code --type file -B 3 -A 3

Repository: storybookjs/storybook

Length of output: 93


🏁 Script executed:

# Look at what react-docgen-typescript documentation says about filePath
# Let's check what the parser actually sets for filePath by looking at test outputs
cat -n code/renderers/react/src/componentManifest/reactDocgen.test.ts | grep -A 30 "filePath"

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

# Check how the similar matching logic works in the react-docgen version
cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.ts | sed -n '93,125p'

Repository: storybookjs/storybook

Length of output: 1371


🏁 Script executed:

# Search for any actual test cases of extractArgTypesFromDocgenTypescript
find code -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "extractArgTypesFromDocgenTypescript" 2>/dev/null

Repository: storybookjs/storybook

Length of output: 49


🏁 Script executed:

# Check what properties the docgen objects from react-docgen-typescript actually have
rg "ComponentDocgenResult|docgens\[" code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts -B 2 -A 5

Repository: storybookjs/storybook

Length of output: 670


🏁 Script executed:

# Let's create a small test to see what react-docgen-typescript actually returns for filePath
# Check if there's any documentation or examples in package.json or node_modules
find code -name "package.json" | xargs grep -l "react-docgen-typescript" | head -1

Repository: storybookjs/storybook

Length of output: 104


🏁 Script executed:

# Let's look at the actual interface expected from react-docgen-typescript
# Check what properties are documented or used
rg "react-docgen-typescript" code/renderers/react --max-count 3 -A 10 -B 2 | head -100

Repository: storybookjs/storybook

Length of output: 8467


🏁 Script executed:

# Let's check if there are multiple components in the same file (which would trigger the matching issue)
rg "export.*Button|export.*Icon" code/renderers/react/src --max-count 10

Repository: storybookjs/storybook

Length of output: 6313


🏁 Script executed:

# Create a Python script to understand the vulnerability more clearly
python3 << 'EOF'
# Demonstrate the substring matching issue
examples = [
    ("Button", "/path/to/Button.tsx"),
    ("Button", "/path/to/IconButton.tsx"),  # FALSE POSITIVE
    ("Button", "/path/to/PrimaryButton.tsx"),  # FALSE POSITIVE
    ("Button", "/path/to/button-icon.tsx"),  # FALSE POSITIVE
    ("Icon", "/path/to/IconButton.tsx"),  # Could match Icon when looking for IconButton
]

print("Substring matching issue with .includes():")
print("-" * 60)
for search, path in examples:
    matches = search in path
    print(f"'{search}' in '{path}': {matches}")
    
print("\n" + "="*60)
print("\nEndsWith fix (more strict):")
print("-" * 60)

extensions = [".tsx", ".ts", ".jsx", ".js"]
for search, path in examples:
    matches = any(path.endswith(f"/{search}{ext}") for ext in extensions)
    print(f"path.endsWith('/{search}.*'): {matches}")
EOF

Repository: storybookjs/storybook

Length of output: 151


🏁 Script executed:

# Let's check how componentFilePath is used - it's a single file being parsed
cat -n code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts | sed -n '108,160p'

Repository: storybookjs/storybook

Length of output: 2234


🏁 Script executed:

# Look for actual cases where multiple components might be in one file (the scenario that triggers the bug)
rg "export.*function|export.*const.*=.*\(\)" code/renderers/react/src/__test__/Button.tsx -B 2 -A 2

Repository: storybookjs/storybook

Length of output: 47


Substring matching on filePath can incorrectly resolve the wrong component.

When multiple components exist in the same file, filePath.includes(componentExportName) will incorrectly match if one component name is a substring of another (e.g., "Button" matches "IconButton"). Since the parallel react-docgen implementation (in extractReactDocgenInfo.ts) relies only on displayName, actualName, and exportName—never on filePath—consider doing the same here. If filePath matching is needed, use a stricter approach (e.g., basename with extension).

🔧 Safer matching (example)
-      const match = docgens.find(
-        (d) => d.displayName === componentExportName || d.filePath?.includes(componentExportName)
-      );
+      const match = docgens.find(
+        (d) =>
+          d.displayName === componentExportName ||
+          d.filePath?.endsWith(`/${componentExportName}.tsx`) ||
+          d.filePath?.endsWith(`/${componentExportName}.ts`) ||
+          d.filePath?.endsWith(`/${componentExportName}.jsx`) ||
+          d.filePath?.endsWith(`/${componentExportName}.js`)
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Find the matching component if a name is specified
let targetDocgen = docgens[0];
if (componentExportName) {
const match = docgens.find(
(d) => d.displayName === componentExportName || d.filePath?.includes(componentExportName)
);
if (match) {
targetDocgen = match;
} else {
// If specific component requested but not found, return null
return null;
// Find the matching component if a name is specified
let targetDocgen = docgens[0];
if (componentExportName) {
const match = docgens.find(
(d) =>
d.displayName === componentExportName ||
d.filePath?.endsWith(`/${componentExportName}.tsx`) ||
d.filePath?.endsWith(`/${componentExportName}.ts`) ||
d.filePath?.endsWith(`/${componentExportName}.jsx`) ||
d.filePath?.endsWith(`/${componentExportName}.js`)
);
if (match) {
targetDocgen = match;
} else {
// If specific component requested but not found, return null
return null;
🤖 Prompt for AI Agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts`
around lines 145 - 155, The current matching logic in
extractReactTypescriptDocgenInfo.ts uses filePath.includes(componentExportName)
which can produce false positives (e.g., "Button" matching "IconButton"); update
the matching to only consider component identifiers like displayName,
actualName, or exportName from each docgen entry: change the predicate in
docgens.find(...) to (d) => d.displayName === componentExportName ||
d.actualName === componentExportName || d.exportName === componentExportName,
removing the filePath.includes check; if you must keep file path matching,
replace it with a strict basename match (compare the file's basename without
extension to componentExportName) rather than substring includes.
Comment on lines +91 to +114
const { componentFilePath, componentExportName, presets } = (options ??
{}) as InternalGetArgTypesDataOptions;

if (!componentFilePath) {
return null;
}

const argTypesData = extractArgTypesFromDocgen({
componentFilePath: path.isAbsolute(componentFilePath)
? componentFilePath
: join(getProjectRoot(), componentFilePath),
componentExportName,
});
// Get typescript options from presets to determine which docgen to use
let typescriptOptions: TypescriptOptionsWithDocgen = {};
if (presets) {
try {
const appliedOptions = await presets.apply('typescript', {});
typescriptOptions = appliedOptions as TypescriptOptionsWithDocgen;
} catch {
// If typescript preset is not available, use defaults
}
}

const { reactDocgen = 'react-docgen', reactDocgenTypescriptOptions } = typescriptOptions;

// If docgen is disabled, return null
if (reactDocgen === false) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -nP '\binternal_getArgTypesData\s*\(' -C 3

Repository: storybookjs/storybook

Length of output: 477


🏁 Script executed:

cd code/renderers/react/src && grep -n "internal_getArgTypesData" preset.ts | head -20

Repository: storybookjs/storybook

Length of output: 115


🏁 Script executed:

fd -e ts -e tsx | xargs rg "internal_getArgTypesData" --no-heading

Repository: storybookjs/storybook

Length of output: 621


🏁 Script executed:

sed -n '1,115p' code/core/src/core-server/utils/get-new-story-file.ts | grep -A 10 "internal_getArgTypesData"

Repository: storybookjs/storybook

Length of output: 504


🏁 Script executed:

sed -n '1,50p' code/addons/vitest/src/vitest-plugin/index.ts | grep -B 5 -A 10 "internal_getArgTypesData"

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

cat code/addons/vitest/src/vitest-plugin/index.ts | grep -B 10 -A 10 "internal_getArgTypesData"

Repository: storybookjs/storybook

Length of output: 615


🏁 Script executed:

cat code/core/src/core-server/utils/get-new-story-file.ts | sed -n '1,150p' | tail -50

Repository: storybookjs/storybook

Length of output: 1618


🏁 Script executed:

cat code/renderers/react/src/preset.ts | sed -n '75,120p'

Repository: storybookjs/storybook

Length of output: 1383


🏁 Script executed:

rg "InternalGetArgTypesDataOptions" -A 10

Repository: storybookjs/storybook

Length of output: 1785


🏁 Script executed:

rg "interface GetArgTypesDataOptions|type GetArgTypesDataOptions" -A 15

Repository: storybookjs/storybook

Length of output: 5311


🏁 Script executed:

cat code/core/src/core-server/utils/get-new-story-file.ts | sed -n '1,100p'

Repository: storybookjs/storybook

Length of output: 4074


🏁 Script executed:

rg "type Options = " -A 30 code/lib/types/src

Repository: storybookjs/storybook

Length of output: 123


🏁 Script executed:

cat code/core/src/core-server/utils/get-new-story-file.ts | sed -n '55,85p'

Repository: storybookjs/storybook

Length of output: 1154


🏁 Script executed:

rg "reactDocgen|reactDocgenTypescriptOptions" code/core/src/core-server/utils/get-new-story-file.ts

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

rg "reactDocgen|reactDocgenTypescriptOptions" code/addons/vitest/src/vitest-plugin/index.ts

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

cat code/renderers/react/src/preset.ts | sed -n '87,160p'

Repository: storybookjs/storybook

Length of output: 1900


🏁 Script executed:

rg "internal_getArgTypesData" --type ts --type tsx -B 2 -A 5 | head -80

Repository: storybookjs/storybook

Length of output: 92


🏁 Script executed:

cat code/renderers/react/src/preset.ts | sed -n '60,90p'

Repository: storybookjs/storybook

Length of output: 1087


🏁 Script executed:

rg "type Options" code/core/src -A 20 | head -60

Repository: storybookjs/storybook

Length of output: 4770


🏁 Script executed:

cat code/addons/vitest/src/vitest-plugin/index.ts | sed -n '1,80p'

Repository: storybookjs/storybook

Length of output: 2587


🏁 Script executed:

rg "reactDocgen:" -B 3 -A 3 code/renderers/react/src

Repository: storybookjs/storybook

Length of output: 3197


🏁 Script executed:

rg "reactDocgenTypescriptOptions:" -B 3 -A 3 code/renderers/react/src

Repository: storybookjs/storybook

Length of output: 47


🏁 Script executed:

cat code/renderers/react/src/preset.ts | sed -n '1,85p'

Repository: storybookjs/storybook

Length of output: 3028


🏁 Script executed:

cat code/renderers/react/src/componentManifest/reactDocgen/utils.ts | sed -n '1,30p'

Repository: storybookjs/storybook

Length of output: 1057


🏁 Script executed:

rg "GetArgTypesDataOptions" -B 2 -A 8

Repository: storybookjs/storybook

Length of output: 8654


🏁 Script executed:

rg "internal_getArgTypesData.*reactDocgen|reactDocgenTypescriptOptions" -B 5 -A 5

Repository: storybookjs/storybook

Length of output: 23943


🏁 Script executed:

rg "presets.apply\('internal_getArgTypesData" -B 5 -A 10

Repository: storybookjs/storybook

Length of output: 2602


Explicit docgen options passed to internal_getArgTypesData are ignored; only preset-provided options are used.

The function accepts reactDocgen and reactDocgenTypescriptOptions via GetArgTypesDataOptions, but never reads them. It only extracts these settings from presets.apply('typescript', {}). When presets aren't available (as in the vitest plugin call site), no fallback to passed options occurs—defaults are silently used instead.

Merge explicit options with preset results, with explicit options taking priority:

🔧 Proposed fix
-      const appliedOptions = await presets.apply('typescript', {});
+      const appliedOptions = await presets.apply('typescript', {}, options);
       typescriptOptions = appliedOptions as TypescriptOptionsWithDocgen;
@@
-  const { reactDocgen = 'react-docgen', reactDocgenTypescriptOptions } = typescriptOptions;
+  const reactDocgen =
+    options?.reactDocgen ?? typescriptOptions.reactDocgen ?? 'react-docgen';
+  const reactDocgenTypescriptOptions =
+    options?.reactDocgenTypescriptOptions ?? typescriptOptions.reactDocgenTypescriptOptions;
🤖 Prompt for AI Agents
In `@code/renderers/react/src/preset.ts` around lines 91 - 114, The code ignores
explicit GetArgTypesDataOptions.reactDocgen and .reactDocgenTypescriptOptions
because it only reads them from presets; update the logic in the function that
uses InternalGetArgTypesDataOptions so that you first collect typescriptOptions
from presets.apply('typescript', {}) (as now) but then merge the explicit
options from the incoming options object into that result with the incoming
options taking priority (i.e., override preset values with options.reactDocgen
and options.reactDocgenTypescriptOptions if they are provided), and ensure the
merged values are then used for the reactDocgen / reactDocgenTypescriptOptions
checks (preserve current fallback to defaults and the reactDocgen === false
early return).
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts`:
- Around line 145-157: The current matching for selecting targetDocgen uses a
substring check (d.filePath?.includes(componentExportName)) which can yield
false positives; update the docgens.find logic (used to set targetDocgen) to
first check d.displayName === componentExportName and then, when matching by
file path, extract the filename without extension (use path.basename and
path.extname) and compare that equality to componentExportName; add an import
for path if missing and keep the existing fallback behavior (return null when no
match).
♻️ Duplicate comments (1)
code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts (1)

53-61: Array generic parsing may fail for nested types.

Similar to the union parsing limitation (already noted in past reviews), the Array<Type> parsing using simple string slicing will fail for types like Array<A | B> or Array<Record<string, number>>. The slice at line 59 would incorrectly extract A | B but would then fail to parse it correctly due to the union handling.

For the current use case with react-docgen-typescript output, this may be acceptable since complex nested types might be represented differently. Consider adding a comment documenting this limitation alongside the union parsing comment.

🧹 Nitpick comments (2)
code/renderers/react/src/componentManifest/reactDocgen/extractReactDocgenInfo.test.ts (1)

43-44: Consider using type assertion or narrowing instead of @ts-expect-error.

Multiple test assertions use @ts-expect-error fix this later to access reactDocgen.data. Since you're already checking result?.reactDocgen.type === 'success' implicitly through the assertion, consider adding a type guard or using a type assertion to make this type-safe:

const data = result?.reactDocgen as { type: 'success'; data: { displayName: string } };
expect(data.data.displayName).toBe('Button');

This is a minor code quality concern and can be addressed later as the comment indicates.

code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts (1)

134-137: Consider adding a comment explaining the jsx compiler option value.

The magic number 2 for jsx option could be more self-documenting:

     const parser = withCompilerOptions(
-      { esModuleInterop: true, jsx: 2 /* React */ },
+      { esModuleInterop: true, jsx: 2 }, // jsx: 2 = ts.JsxEmit.React
       mergedOptions
     );

The inline comment /* React */ is helpful but could be expanded to reference ts.JsxEmit.React for developers unfamiliar with the TypeScript compiler API.

Comment on lines +145 to +157
// Find the matching component if a name is specified
let targetDocgen = docgens[0];
if (componentExportName) {
const match = docgens.find(
(d) => d.displayName === componentExportName || d.filePath?.includes(componentExportName)
);
if (match) {
targetDocgen = match;
} else {
// If specific component requested but not found, return null
return null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Weak component matching via filePath.includes() may cause false positives.

The matching logic at line 149 uses d.filePath?.includes(componentExportName), which is a substring match. This could incorrectly match components when the export name is a substring of another file path:

  • componentExportName = "Button" would match filePath = "/app/ButtonGroup.tsx"
  • componentExportName = "Icon" would match filePath = "/app/IconButton.tsx"

Consider using a more precise match, such as checking if the filename (without extension) equals the component name:

🔧 Suggested improvement
     if (componentExportName) {
       const match = docgens.find(
-        (d) => d.displayName === componentExportName || d.filePath?.includes(componentExportName)
+        (d) => d.displayName === componentExportName
       );

Or if filePath matching is needed, use a more precise check:

const match = docgens.find((d) => {
  if (d.displayName === componentExportName) return true;
  if (d.filePath) {
    const fileName = path.basename(d.filePath, path.extname(d.filePath));
    return fileName === componentExportName;
  }
  return false;
});
🤖 Prompt for AI Agents
In
`@code/renderers/react/src/componentManifest/reactDocgen/extractReactTypescriptDocgenInfo.ts`
around lines 145 - 157, The current matching for selecting targetDocgen uses a
substring check (d.filePath?.includes(componentExportName)) which can yield
false positives; update the docgens.find logic (used to set targetDocgen) to
first check d.displayName === componentExportName and then, when matching by
file path, extract the filename without extension (use path.basename and
path.extname) and compare that equality to componentExportName; add an import
for path if missing and keep the existing fallback behavior (return null when no
match).
Copy link
Member

@ndelangen ndelangen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not been able to look carefully through every LOC here.

Particularly, the extractReactDocgenInfo.ts & extractReactTypescriptDocgenInfo.ts are too much to digest the implementation of. I'm not sure what we can do to get higher degree of confidence on this right now.
I can see there are unit tests, so I trust that coverage is good, and this is well tested and QA-ed.

But I see the if-else and the getting options via preset.apply and that look looks correct to me.

@ndelangen ndelangen merged commit bc371ca into next Jan 19, 2026
179 of 182 checks passed
@ndelangen ndelangen deleted the yann/story-creation-docgen-ts branch January 19, 2026 16:36
@github-actions github-actions bot mentioned this pull request Jan 19, 2026
18 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug ci:merged Run the CI jobs that normally run when merged. core

3 participants