ReactNative: Add Metro config AST codemod for init#34660
Conversation
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
|
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:
📝 WalkthroughWalkthroughIntroduces a new React Native Metro config codemod module with comprehensive test coverage that automatically identifies and rewrites Changes
Sequence DiagramsequenceDiagram
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
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts (2)
8-14: ⚡ Quick winAddress the TODO placeholder for documentation link.
METRO_SETUP_DOCS_LINKis 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 valueRemove unused constant
STORYBOOK_PACKAGE_PATTERNS.
STORYBOOK_PACKAGE_PATTERNSis defined but never used. ThehasStorybookPackagefunction below reimplements the same logic inline. The only reference is in the fallback string search at line 103, which could usehasStorybookPackageor 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 valueConsider extracting prompt mock setup to a helper or beforeEach for complex scenarios.
The inline
vi.mocked(prompt.select).mockResolvedValue(...)andvi.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
beforeEachblocks 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
📒 Files selected for processing (2)
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.tscode/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts
There was a problem hiding this comment.
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.tswith AST parsing/printing (recast + Babel parser) and arunMetroCodemodOrFallback()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.
Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/a0051484-2abf-4a87-be9a-e452207b163a Co-authored-by: ndelangen <3070389+ndelangen@users.noreply.github.com>
…ting withStorybook import Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/a0051484-2abf-4a87-be9a-e452207b163a Co-authored-by: ndelangen <3070389+ndelangen@users.noreply.github.com>
Agent-Logs-Url: https://github.com/storybookjs/storybook/sessions/a0051484-2abf-4a87-be9a-e452207b163a Co-authored-by: ndelangen <3070389+ndelangen@users.noreply.github.com>
…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>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts (1)
9-10:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winReplace 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
📒 Files selected for processing (2)
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.test.tscode/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
…y in metroConfig.ts
… in metroConfig.ts
There was a problem hiding this comment.
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
📒 Files selected for processing (1)
code/lib/create-storybook/src/generators/REACT_NATIVE/metroConfig.ts
…tro configuration documentation
…e. Added tests for reusing aliased ESM and CJS imports, and updated isWithStorybookCall to accept dynamic local names.
Package BenchmarksCommit: The following packages have significant changes to their size or dependencies:
|
| 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 |
…oss various statement forms in React Native. Added additional test cases for improved coverage of dynamic and aliased imports.
Split out of #34333 (M1).
Tracking issue: #34276.
What I did
Adds an AST-based codemod (using
recast+@babel/parservia the existingstorybook/internal/babelre-export) that wraps a project'smetro.config.{js,ts,cjs}export withwithStorybook(...)and adds the matching import.Handles:
module.exports = ...) and ESM (export default ...)asyncfunctionsAlso 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:
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
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 PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the
@storybookjs/coreteam 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
Tests