Skip to content

ReactNative: Add Metro config AST codemod for init#34660

Merged
ndelangen merged 16 commits into
nextfrom
norbert/m1-metro-config-codemod
May 5, 2026
Merged

ReactNative: Add Metro config AST codemod for init#34660
ndelangen merged 16 commits into
nextfrom
norbert/m1-metro-config-codemod

Conversation

@ndelangen
Copy link
Copy Markdown
Member

@ndelangen ndelangen commented Apr 30, 2026

Split out of #34333 (M1).
Tracking issue: #34276.

What I did

Adds an AST-based codemod (using recast + @babel/parser via the existing storybook/internal/babel re-export) that wraps a project's metro.config.{js,ts,cjs} export with withStorybook(...) and adds the matching import.

Handles:

  • CommonJS (module.exports = ...) and ESM (export default ...)
  • Direct config objects, function declarations, and async functions
  • TypeScript sources
  • Idempotency (won't double-wrap configs that already include Storybook)

Also exports a fallback path that prepends a guidance comment to the metro config when the AST transform can't be applied safely.

This module is intentionally a self-contained, tested utility — it is not yet wired into the React Native generator. That orchestration arrives in M3.

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

I've manually run the init code via a canary many times to ensure the output is correct, and the metro config file is generated+modified correctly

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 PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

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

Summary by CodeRabbit

  • New Features

    • Automated React Native Metro config integration: Storybook now automatically updates metro configurations, supporting CommonJS/ESM, TypeScript, and Expo projects.
  • Tests

    • Added comprehensive test suite validating Metro config codemod behavior across different scenarios and edge cases.

Adds an AST-based codemod (using `recast` + `@babel/parser` via the existing
`storybook/internal/babel` re-export) that wraps a project's `metro.config.{js,ts,cjs}`
export with `withStorybook(...)` and adds the matching import.

Handles:
- CommonJS (`module.exports = ...`) and ESM (`export default ...`)
- Direct config objects, function declarations, and `async` functions
- TypeScript sources
- Idempotency (won't double-wrap configs that already include Storybook)

Also exports a fallback path that prepends a guidance comment to the metro
config when the AST transform can't be applied safely.

This module is intentionally a self-contained, tested utility — it is not
yet wired into the React Native generator. That orchestration arrives in M3.

Split out of #34333 (M1). Tracking issue: #34276.

Made-with: Cursor
Copilot AI review requested due to automatic review settings April 30, 2026 07:44
@ndelangen ndelangen self-assigned this Apr 30, 2026
Comment thread code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a new React Native Metro config codemod module with comprehensive test coverage that automatically identifies and rewrites metro.config.* files to wrap their exports with Storybook's withStorybook function, supporting both CommonJS and ESM variants while handling edge cases like missing files and unsupported shapes.

Changes

Cohort / File(s) Summary
React Native Metro Config Codemod
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts, code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts
New codemod implementation and test suite for transforming Metro configurations. Implementation includes AST parsing, ESM/CJS import detection, config wrapping with withStorybook, fallback comment insertion for unsupported shapes, and Expo metro config generation. Tests verify end-to-end behavior across CJS/ESM/TypeScript scenarios, idempotency, interactive prompting, and edge cases like preserving directives and handling existing Storybook imports.

Sequence Diagram

sequenceDiagram
    actor User
    participant Generator as Metro<br/>Codemod
    participant FS as File<br/>System
    participant AST as AST<br/>Parser
    participant PM as Package<br/>Manager
    participant Expo as Expo<br/>CLI

    User->>Generator: runMetroCodemodOrFallback()
    Generator->>FS: Find metro.config.*
    alt Metro config not found
        Generator->>PM: Check if expo installed
        PM-->>Generator: expo info
        opt Expo available
            Generator->>Expo: expo customize metro.config
            Expo-->>FS: Create metro.config.js
            FS-->>Generator: File created
        end
    end
    Generator->>FS: Read metro config source
    FS-->>Generator: Source code
    Generator->>Generator: containsStorybookImport()
    alt Storybook already present
        Generator-->>User: MetroCodemodResult(skipped)
    else No Storybook import
        Generator->>AST: Parse source to AST
        alt Parse successful
            Generator->>Generator: transformMetroConfigSource()
            Generator->>AST: Identify exports & wrapping
            Generator->>Generator: Inject import/require + wrap
            Generator-->>FS: Write transformed source
            FS-->>Generator: File updated
            Generator-->>User: MetroCodemodResult(updated)
        else Parse failed
            Generator->>Generator: prependMetroFallbackComment()
            Generator-->>FS: Write fallback comment
            FS-->>Generator: File updated
            Generator-->>User: MetroCodemodResult(fallback)
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

Copy link
Copy Markdown
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.

🧹 Nitpick comments (3)
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts (2)

8-14: ⚡ Quick win

Address the TODO placeholder for documentation link.

METRO_SETUP_DOCS_LINK is set to a placeholder value. This will display a non-functional link to users in the fallback comment when the codemod cannot be applied automatically.

Would you like me to open an issue to track replacing this with the actual React Native Metro setup documentation link?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts` around
lines 8 - 14, Replace the placeholder METRO_SETUP_DOCS_LINK value with the real
React Native Metro setup documentation URL so the fallback comment produced by
the codemod points to a working doc; update the constant METRO_SETUP_DOCS_LINK
in the file (refer to the exported symbol METRO_SETUP_DOCS_LINK) to the
authoritative Metro configuration docs (or Expo metro docs if more appropriate)
and ensure the string is a fully-qualified HTTPS URL.

34-38: 💤 Low value

Remove unused constant STORYBOOK_PACKAGE_PATTERNS.

STORYBOOK_PACKAGE_PATTERNS is defined but never used. The hasStorybookPackage function below reimplements the same logic inline. The only reference is in the fallback string search at line 103, which could use hasStorybookPackage or the constant directly for consistency.

♻️ Option 1: Remove unused constant and use inline patterns in fallback
-const STORYBOOK_PACKAGE_PATTERNS = ['@storybook/', 'storybook', 'storybook/'];
-
 const hasStorybookPackage = (value: string) => {
   return value === 'storybook' || value.startsWith('@storybook/') || value.startsWith('storybook/');
 };

Then update line 103:

   } catch {
-    return STORYBOOK_PACKAGE_PATTERNS.some((pattern) => source.includes(pattern));
+    return source.includes('@storybook/') || source.includes('storybook/') || source.includes("'storybook'");
   }
♻️ Option 2: Reuse the constant in hasStorybookPackage
 const STORYBOOK_PACKAGE_PATTERNS = ['@storybook/', 'storybook/', 'storybook'] as const;

 const hasStorybookPackage = (value: string) => {
-  return value === 'storybook' || value.startsWith('@storybook/') || value.startsWith('storybook/');
+  return STORYBOOK_PACKAGE_PATTERNS.some(
+    (pattern) => value === pattern || value.startsWith(pattern + (pattern.endsWith('/') ? '' : '/'))
+  );
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts` around
lines 34 - 38, Remove the unused duplicate logic by reusing
STORYBOOK_PACKAGE_PATTERNS: replace the inline checks in hasStorybookPackage
with a loop/Array.prototype.some over STORYBOOK_PACKAGE_PATTERNS (checking
equality or startsWith as appropriate), and update the fallback string search
that currently reimplements the logic to call hasStorybookPackage (or use
STORYBOOK_PACKAGE_PATTERNS directly) so the pattern array is the single source
of truth; ensure function name hasStorybookPackage and constant
STORYBOOK_PACKAGE_PATTERNS are referenced and preserved.
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts (1)

254-321: 💤 Low value

Consider extracting prompt mock setup to a helper or beforeEach for complex scenarios.

The inline vi.mocked(prompt.select).mockResolvedValue(...) and vi.mocked(prompt.text).mockResolvedValue(...) calls work but could be consolidated. Since each test requires different mock return values, inline mocks are pragmatic here. This is a minor style consideration.

As per coding guidelines: "Implement mock behaviors in beforeEach blocks in Vitest tests" — though in this case, test-specific return values make inline mocks reasonable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts`
around lines 254 - 321, Extract the repeated prompt mocking into a shared helper
or a beforeEach: move common setup of vi.mocked(prompt.select) and
vi.mocked(prompt.text) into a beforeEach (or a helper function used by tests)
that provides default resolved values, and let individual tests override those
defaults (e.g., in the "prompts for file selection when multiple metro configs
exist" test override prompt.select, and in "uses prompt path for missing
non-expo metro config" override prompt.text); update references to prompt.select
and prompt.text and keep runMetroCodemodOrFallback calls unchanged so tests
remain focused while mock setup is centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts`:
- Around line 254-321: Extract the repeated prompt mocking into a shared helper
or a beforeEach: move common setup of vi.mocked(prompt.select) and
vi.mocked(prompt.text) into a beforeEach (or a helper function used by tests)
that provides default resolved values, and let individual tests override those
defaults (e.g., in the "prompts for file selection when multiple metro configs
exist" test override prompt.select, and in "uses prompt path for missing
non-expo metro config" override prompt.text); update references to prompt.select
and prompt.text and keep runMetroCodemodOrFallback calls unchanged so tests
remain focused while mock setup is centralized.

In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts`:
- Around line 8-14: Replace the placeholder METRO_SETUP_DOCS_LINK value with the
real React Native Metro setup documentation URL so the fallback comment produced
by the codemod points to a working doc; update the constant
METRO_SETUP_DOCS_LINK in the file (refer to the exported symbol
METRO_SETUP_DOCS_LINK) to the authoritative Metro configuration docs (or Expo
metro docs if more appropriate) and ensure the string is a fully-qualified HTTPS
URL.
- Around line 34-38: Remove the unused duplicate logic by reusing
STORYBOOK_PACKAGE_PATTERNS: replace the inline checks in hasStorybookPackage
with a loop/Array.prototype.some over STORYBOOK_PACKAGE_PATTERNS (checking
equality or startsWith as appropriate), and update the fallback string search
that currently reimplements the logic to call hasStorybookPackage (or use
STORYBOOK_PACKAGE_PATTERNS directly) so the pattern array is the single source
of truth; ensure function name hasStorybookPackage and constant
STORYBOOK_PACKAGE_PATTERNS are referenced and preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5eb0afbf-babb-48a9-bdbd-1d378f08e0d0

📥 Commits

Reviewing files that changed from the base of the PR and between cf07244 and 9ae8eb7.

📒 Files selected for processing (2)
  • code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts
  • code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a self-contained, unit-tested Metro config codemod utility for React Native storybook init to wrap the exported Metro config with withStorybook(...), inject the corresponding import/require, and fall back to a guidance comment when an AST-safe transform isn’t possible.

Changes:

  • Introduces metroConfig.ts with AST parsing/printing (recast + Babel parser) and a runMetroCodemodOrFallback() entrypoint.
  • Adds fallback comment injection for unsupported/failed transforms.
  • Adds Vitest unit tests covering CJS/ESM/TS cases, wrapper composition, and idempotency behaviors.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts Implements Metro config detection, AST transform, import injection, and fallback behavior.
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts Adds unit tests for the codemod and fallback logic across export styles and file types.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts Outdated
Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/a0051484-2abf-4a87-be9a-e452207b163a

Co-authored-by: ndelangen <3070389+ndelangen@users.noreply.github.com>
Copilot AI and others added 2 commits April 30, 2026 10:01
…e positives on storybook-like identifiers

Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/dc19a855-5aed-4ae7-8b3d-cd7536405dd0

Co-authored-by: ndelangen <3070389+ndelangen@users.noreply.github.com>
Copy link
Copy Markdown
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

♻️ Duplicate comments (1)
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts (1)

9-10: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the placeholder Metro docs link before merge.

Line 9 still uses TODO_REPLACE_WITH_REACT_NATIVE_METRO_DOCS_LINK, and that exact placeholder is surfaced to users in fallback guidance (Line 279). If the AST transform falls back, instructions are non-actionable.

Also applies to: 279-280

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts` around
lines 9 - 10, The exported constant METRO_SETUP_DOCS_LINK currently contains the
placeholder TODO_REPLACE_WITH_REACT_NATIVE_METRO_DOCS_LINK which is surfaced to
users in fallback guidance; replace that placeholder with the real React Native
Metro configuration docs URL (so users see actionable instructions when the AST
transform falls back), and keep METRO_FALLBACK_COMMENT_MARKER unchanged — update
any user-facing text that concatenates METRO_SETUP_DOCS_LINK (e.g., the fallback
guidance message) to ensure it now points to the real documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts`:
- Around line 85-117: containsStorybookImport currently only checks
ImportDeclaration and VariableDeclaration initialized with require(...), missing
patterns like inline require(...) used in CallExpressions,
AssignmentExpressions, ExportNamedDeclarations, or MemberExpressions (e.g.
require('...').withStorybook()). Update containsStorybookImport to also traverse
top-level statements and detect
CallExpression/AssignmentExpression/ExpressionStatement/ExportNamedDeclaration
forms where a CallExpression exists whose callee is require or whose callee is a
MemberExpression whose object is a require(...) CallExpression; for any found
CallExpression grab its first string literal argument and run
hasStorybookPackage on it, returning true if matched. Keep the existing
try/catch fallback to STORYBOOK_PACKAGE_PATTERNS.

---

Duplicate comments:
In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts`:
- Around line 9-10: The exported constant METRO_SETUP_DOCS_LINK currently
contains the placeholder TODO_REPLACE_WITH_REACT_NATIVE_METRO_DOCS_LINK which is
surfaced to users in fallback guidance; replace that placeholder with the real
React Native Metro configuration docs URL (so users see actionable instructions
when the AST transform falls back), and keep METRO_FALLBACK_COMMENT_MARKER
unchanged — update any user-facing text that concatenates METRO_SETUP_DOCS_LINK
(e.g., the fallback guidance message) to ensure it now points to the real
documentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6ee9565-ba40-4e2c-8559-3df5125fe668

📥 Commits

Reviewing files that changed from the base of the PR and between bc95b8c and e03767c.

📒 Files selected for processing (2)
  • code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts
  • code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts
✅ Files skipped from review due to trivial changes (1)
  • code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.ts

@ndelangen ndelangen requested review from dannyhw and shilman April 30, 2026 10:36
@ndelangen ndelangen mentioned this pull request Apr 30, 2026
6 tasks
Copy link
Copy Markdown
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts`:
- Around line 58-60: The transform treats any imported withStorybook as reusable
but always emits the literal identifier "withStorybook", which breaks when the
import was aliased; update the detection and emission to resolve and reuse the
actual local binding name: change hasWithStorybookBinding to return the local
identifier name (or undefined), modify isWithStorybookCall to accept/compare
against that resolved local name instead of hardcoding 'withStorybook', and
update transformMetroConfigSource (and the wrapping logic that currently emits
withStorybook(...) in the same module) to use the resolved local identifier when
generating call expressions; alternatively, if you prefer stricter behavior,
restrict hasWithStorybookBinding to only return true for an unaliased import and
leave emission as-is, but be explicit about that choice in the binding check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: faacba37-8c2f-487a-9625-672ffd32bc41

📥 Commits

Reviewing files that changed from the base of the PR and between e03767c and 2010469.

📒 Files selected for processing (1)
  • code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts

Comment thread code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts Outdated
@ndelangen ndelangen requested a review from JReinhold April 30, 2026 11:59
Comment thread code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts Outdated
ndelangen added 2 commits May 4, 2026 17:06
…e. Added tests for reusing aliased ESM and CJS imports, and updated isWithStorybookCall to accept dynamic local names.
@storybook-app-bot
Copy link
Copy Markdown

storybook-app-bot Bot commented May 4, 2026

Package Benchmarks

Commit: 62cf908, ran on 4 May 2026 at 15:50:59 UTC

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

@storybook/nextjs-vite

Before After Difference
Dependency count 93 93 0
Self size 1.12 MB 1.12 MB 0 B
Dependency size 23.78 MB 24.67 MB 🚨 +891 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-native-web-vite

Before After Difference
Dependency count 122 122 0
Self size 30 KB 30 KB 0 B
Dependency size 24.85 MB 25.74 MB 🚨 +891 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-vite

Before After Difference
Dependency count 83 83 0
Self size 36 KB 36 KB 🚨 +18 B 🚨
Dependency size 21.56 MB 22.45 MB 🚨 +891 KB 🚨
Bundle Size Analyzer Link Link

ndelangen added 2 commits May 4, 2026 17:20
…oss various statement forms in React Native. Added additional test cases for improved coverage of dynamic and aliased imports.
@ndelangen ndelangen merged commit 860a773 into next May 5, 2026
122 checks passed
@ndelangen ndelangen deleted the norbert/m1-metro-config-codemod branch May 5, 2026 07:35
@github-actions github-actions Bot mentioned this pull request May 5, 2026
10 tasks
@github-actions github-actions Bot mentioned this pull request May 5, 2026
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants