Skip to content

Automigration: Move RN on-device addons to deviceAddons#34659

Merged
ndelangen merged 8 commits into
nextfrom
norbert/m8-device-addons-automigration
May 4, 2026
Merged

Automigration: Move RN on-device addons to deviceAddons#34659
ndelangen merged 8 commits into
nextfrom
norbert/m8-device-addons-automigration

Conversation

@ndelangen
Copy link
Copy Markdown
Member

@ndelangen ndelangen commented Apr 30, 2026

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

What I did

Adds a new rn-ondevice-addons-to-device-addons automigration for React Native projects that renames the addons key in .rnstorybook/main.ts to deviceAddons.

Storybook Core evaluates every entry in addons as a Node.js preset. On-device addons (@storybook/addon-ondevice-*) are not Node.js presets, and listing them under addons causes storybook extract to fail.

Change of heart

The first iteration of this codemod tried to be clever: it scanned the addons array, detected on-device entries by matching the substring ondevice in their package name, and moved only those entries into a new deviceAddons array, leaving everything else under addons.

After discussing it with @shilman, we decided that the approach was both too narrow and too presumptuous:

  • React Native's .rnstorybook/main.ts has no use for the regular addons key in the first place: RN does not run a Storybook builder, so nothing under addons would ever be evaluated as a Node.js preset usefully. In practice, anything you list there is a "device addon".
  • The on-device substring heuristic would silently miss any community/third-party on-device addon that doesn't follow that naming convention.
  • "Split this array based on a string match" is a more fragile transform than "rename this key", and it produces a less obvious diff for users to review.

So the new behavior is simpler: rename the addons key to deviceAddons. The whole array moves verbatim — strings, object-form entries, comments (best-effort, as with all our AST transforms) — and no entry is dropped or reclassified.

How it works now

  1. Bail out if @storybook/react-native is not in the project's dependencies.
  2. Build the set of candidate main.ts files: the active config plus a sibling when the project has both .storybook/ and .rnstorybook/ (the React-Native + RNW setup).
  3. For each candidate, decide whether it is a React Native main. A main is RN if EITHER:
    • it lives in a directory named .rnstorybook (the RN_STORYBOOK_DIR constant), OR
    • its framework field resolves to @storybook/react-native.
      Anything else — notably @storybook/react-native-web-vite or any other web framework — is treated as a web main and skipped.
  4. Keep the candidate only if its addons field is a non-empty array AND deviceAddons is not already present (idempotency: re-running the migration is a no-op).

For every selected target, perform an AST-level key rename:

const node = main.getFieldNode(['addons']);
if (node) {
  main.setFieldNode(['deviceAddons'], node);
  main.removeField(['addons']);
}

This means in a paired RN + RNW setup, only .rnstorybook/main.ts is migrated; the web .storybook/main.ts is left completely untouched.

MIGRATION.md has been rewritten to describe the simpler "rename the whole key" behavior, with a matching before/after example.

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

Manually ran the migration on:

  • an RN-only project with an addons array containing both @storybook/addon-ondevice-controls and @storybook/addon-docs — both moved under deviceAddons, original key removed.
  • a paired .storybook/ (web, @storybook/react-native-web-vite) + .rnstorybook/ setup — only .rnstorybook/main.ts was modified.
  • a project where deviceAddons was already defined — no-op, as expected.

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

More information
Published version 0.0.0-pr-34659-sha-a82cc511
Triggered by @ndelangen
Repository storybookjs/storybook
Branch norbert/m8-device-addons-automigration
Commit a82cc511
Datetime Thu Apr 30 09:46:43 UTC 2026 (1777542403)
Workflow run 25158756655

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=34659

Summary by CodeRabbit

  • New Features

    • Added automatic migration support for React Native on-device addon configuration in Storybook 10.4. Automigration moves addon entries from addons to deviceAddons in React Native on-device main configs.
  • Documentation

    • New migration guide for upgrading to Storybook 10.4 with React Native on-device configurations, including manual migration steps and code examples.

…dons`

Adds a new `rn-ondevice-addons-to-device-addons` automigration that moves
on-device addons (packages whose name contains `ondevice`) from the shared
`addons` array into a new `deviceAddons` array in `.rnstorybook/main.ts`.

This is needed because Storybook Core evaluates every entry in `addons` as
a Node.js preset, which on-device addons are not, and that causes
`storybook extract` to fail.

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

Made-with: Cursor
Copilot AI review requested due to automatic review settings April 30, 2026 07:32
@ndelangen ndelangen self-assigned this Apr 30, 2026
@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

Adds a new automigration fix for React Native on-device configurations. The change includes documentation explaining the migration of addons to deviceAddons in .rnstorybook/main.ts, implementation of the detection and transformation logic, corresponding tests, and registration within the automigration system.

Changes

Cohort / File(s) Summary
Documentation
MIGRATION.md
Adds migration guide for upgrading to Storybook 10.4, explaining that React Native on-device addon configs must move entries from addons to deviceAddons and documenting the automigration behavior.
Automigration Infrastructure
code/lib/cli-storybook/src/automigrate/fixes/index.ts
Registers the new rn-ondevice-addons-to-device-addons fix in the automigration registry.
React Native On-Device Addons Migration
code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts, code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts
Implements detection and transformation of React Native main configs, checking for @storybook/react-native dependency, identifying configs with addons but no deviceAddons, and renaming the AST field during migration. Comprehensive test suite verifies check/run phases with various config scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • storybook#34500: Implements the same React Native automigration fix with identical documentation, implementation, and test coverage.
  • storybook#32717: Adds other new automigration fixes and extends the automigrate system registry, following the same patterns as this PR's fix registration approach.

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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts (1)

25-46: ⚡ Quick win

Align Vitest mocks with repository spy-mocking rules.

These module mocks use factory-only vi.mock(...) patterns. The repo guideline requires spy-based mocks and behavior setup through beforeEach for consistency and typing safety.

As per coding guidelines, “Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests” and “Implement mock behaviors in beforeEach blocks in Vitest tests.”

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

In
`@code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts`
around lines 25 - 46, Replace the factory-only mocks with spy-based mocks and
move behavior setup into beforeEach: call vi.mock('node:fs', { spy: true }) and
in beforeEach set a spy implementation for existsSync to use
mocks.existsSyncOverride or the real existsSync;
vi.mock('../helpers/mainConfigFile', { spy: true }) and in beforeEach set
updateMainConfig to mocks.updateMainConfig; and
vi.mock('storybook/internal/common', { spy: true }) and in beforeEach set spies
for findConfigFile and loadMainConfig to call the original implementations or
test doubles as needed. Target the mocked symbols existsSync, updateMainConfig,
findConfigFile, and loadMainConfig and ensure all behavioral wiring lives inside
beforeEach for typing and repo consistency.
🤖 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/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts`:
- Around line 58-59: The mocks for findConfigFile and loadMainConfig are
implemented by returning the mocked symbols themselves, which creates
self-referential recursion; update the test to provide the real implementations
instead (e.g., capture the unmocked implementations via
vi.importActual/vi.requireActual or store originals before mocking) and use
those in vi.mocked(...).mockImplementation(...) for findConfigFile and
loadMainConfig so fallback branches call the real functions rather than the
mocks (refer to the mocked symbols findConfigFile and loadMainConfig in the
test).

In
`@code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts`:
- Around line 130-139: The loop in run iterates all result.targets including
ones where ondeviceAddons is an empty array, causing unnecessary calls to
updateMainConfig; modify run to skip targets with no ondeviceAddons by checking
each target's ondeviceAddons length (e.g., if (!ondeviceAddons ||
ondeviceAddons.length === 0) continue) before calling updateMainConfig so only
targets with actual addons are processed; updateMainConfig, run, result.targets
and ondeviceAddons are the symbols to locate and change.

---

Nitpick comments:
In
`@code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts`:
- Around line 25-46: Replace the factory-only mocks with spy-based mocks and
move behavior setup into beforeEach: call vi.mock('node:fs', { spy: true }) and
in beforeEach set a spy implementation for existsSync to use
mocks.existsSyncOverride or the real existsSync;
vi.mock('../helpers/mainConfigFile', { spy: true }) and in beforeEach set
updateMainConfig to mocks.updateMainConfig; and
vi.mock('storybook/internal/common', { spy: true }) and in beforeEach set spies
for findConfigFile and loadMainConfig to call the original implementations or
test doubles as needed. Target the mocked symbols existsSync, updateMainConfig,
findConfigFile, and loadMainConfig and ensure all behavioral wiring lives inside
beforeEach for typing and repo consistency.
🪄 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: f0088d71-d90f-4bb5-8fd7-51936e97c793

📥 Commits

Reviewing files that changed from the base of the PR and between cf07244 and 48bfaa9.

📒 Files selected for processing (4)
  • MIGRATION.md
  • code/lib/cli-storybook/src/automigrate/fixes/index.ts
  • code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts
  • code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts

This comment was marked as outdated.

@ndelangen ndelangen mentioned this pull request Apr 30, 2026
20 tasks
Updated the migration documentation for React Native to specify that on-device addons must now be listed under the `deviceAddons` key in `.rnstorybook/main.ts`. Enhanced the automigration logic to ensure it correctly identifies and renames the `addons` key to `deviceAddons`, while also improving test coverage for various scenarios related to the migration process.
@ndelangen ndelangen requested review from dannyhw and shilman April 30, 2026 09:43
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 (2)
code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts (1)

29-53: 💤 Low value

Add spy: true option to vi.mock() calls.

As per coding guidelines, vi.mock() should use the spy: true option for all package and file mocks. This applies to the mocks for node:fs, ../helpers/mainConfigFile, and storybook/internal/common.

♻️ Example fix for one mock
-vi.mock('storybook/internal/common', async (importOriginal) => {
+vi.mock('storybook/internal/common', { spy: true }, async (importOriginal) => {
   const mod = await importOriginal<typeof import('storybook/internal/common')>();
   return {
     ...mod,
     findConfigFile: vi.fn(() => undefined),
     loadMainConfig: vi.fn(),
   };
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts`
around lines 29 - 53, The vi.mock calls in this test file lack the required spy
option; update each vi.mock invocation for 'node:fs',
'../helpers/mainConfigFile', and 'storybook/internal/common' to pass { spy: true
} as the second argument so the original module is spied on; leave the async
factory functions and returned overrides (the existsSync override that uses
mocks.existsSyncOverride, the updateMainConfig override, and the
findConfigFile/loadMainConfig spies) intact but add the { spy: true } option to
each vi.mock call signature.
code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts (1)

10-10: 💤 Low value

Consider using a package import instead of a cross-package relative path.

This deep relative path (../../../../../core/src/...) crosses from cli-storybook into core's internal structure. If core restructures, this import breaks. If RN_STORYBOOK_DIR is exported via storybook/internal/common or a similar package path, prefer that for consistency with the other imports in this file.

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

In
`@code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts`
at line 10, The import uses a fragile cross-package relative path for
RN_STORYBOOK_DIR; replace that relative import with the stable package export
(e.g. import RN_STORYBOOK_DIR from 'storybook/internal/common' or whatever
public module in core exports it) so the module no longer depends on core's
internal folder layout; update the import statement in
rn-ondevice-addons-to-device-addons.ts to reference the package-level export
that re-exports RN_STORYBOOK_DIR.
🤖 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/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts`:
- Around line 29-53: The vi.mock calls in this test file lack the required spy
option; update each vi.mock invocation for 'node:fs',
'../helpers/mainConfigFile', and 'storybook/internal/common' to pass { spy: true
} as the second argument so the original module is spied on; leave the async
factory functions and returned overrides (the existsSync override that uses
mocks.existsSyncOverride, the updateMainConfig override, and the
findConfigFile/loadMainConfig spies) intact but add the { spy: true } option to
each vi.mock call signature.

In
`@code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts`:
- Line 10: The import uses a fragile cross-package relative path for
RN_STORYBOOK_DIR; replace that relative import with the stable package export
(e.g. import RN_STORYBOOK_DIR from 'storybook/internal/common' or whatever
public module in core exports it) so the module no longer depends on core's
internal folder layout; update the import statement in
rn-ondevice-addons-to-device-addons.ts to reference the package-level export
that re-exports RN_STORYBOOK_DIR.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2520fe56-e610-4cc6-b0cf-8ad5038c3391

📥 Commits

Reviewing files that changed from the base of the PR and between 48bfaa9 and a82cc51.

📒 Files selected for processing (3)
  • MIGRATION.md
  • code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts
  • code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts
✅ Files skipped from review due to trivial changes (1)
  • MIGRATION.md

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 new CLI automigration to fix React Native on-device Storybook configs by renaming the addons field in .rnstorybook/main.* to deviceAddons, preventing RN on-device addons from being evaluated as Node.js presets during tasks like storybook extract.

Changes:

  • Introduces rn-ondevice-addons-to-device-addons automigration that targets RN mains and renames addonsdeviceAddons via AST transforms.
  • Adds unit tests covering RN vs web main detection and the key-rename behavior.
  • Updates MIGRATION.md with a new 10.3 → 10.4 entry documenting the change and automigration.

Reviewed changes

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

File Description
code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts Implements the automigration: detects RN mains and renames addonsdeviceAddons.
code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.test.ts Adds unit tests for selection logic (RN vs RNW) and AST rename behavior.
code/lib/cli-storybook/src/automigrate/fixes/index.ts Registers the new fix in the allFixes list.
MIGRATION.md Documents the migration and provides before/after examples for RN configs.

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

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/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts`:
- Around line 53-63: Replace the value-based idempotency check in
hasAddonsToRename so it detects the presence of the deviceAddons property
regardless of its value by using Object.prototype.hasOwnProperty.call(cfg,
'deviceAddons') instead of (cfg as { deviceAddons?: unknown }).deviceAddons !==
undefined; and add a defensive presence check at the start of the run() function
(the function that mutates cfg to set deviceAddons) to skip mutation (or bail)
if Object.prototype.hasOwnProperty.call(cfg, 'deviceAddons') is true to avoid
clobbering an existing field.
🪄 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: 27c5711d-c7bc-4cb1-8161-5b26b550b18b

📥 Commits

Reviewing files that changed from the base of the PR and between a82cc51 and 0ff17cf.

📒 Files selected for processing (3)
  • code/core/src/manager/globals/exports.ts
  • code/lib/cli-storybook/src/automigrate/fixes/rn-ondevice-addons-to-device-addons.ts
  • docs/configure/integration/eslint-plugin.mdx
💤 Files with no reviewable changes (1)
  • code/core/src/manager/globals/exports.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/configure/integration/eslint-plugin.mdx

@ndelangen ndelangen mentioned this pull request Apr 30, 2026
6 tasks
This change modifies the mock implementation of `findConfigFile` in the `rn-ondevice-addons-to-device-addons` test to return `null` instead of `undefined`, ensuring more accurate test behavior.
This change updates the type casting for the `node` variable in the `rn-ondevice-addons-to-device-addons` migration logic, ensuring compatibility when setting the `deviceAddons` field in the main configuration.
@ndelangen ndelangen requested a review from JReinhold April 30, 2026 11:59
Comment thread MIGRATION.md
@ndelangen ndelangen merged commit 1cf16c5 into next May 4, 2026
122 checks passed
@ndelangen ndelangen deleted the norbert/m8-device-addons-automigration branch May 4, 2026 07:29
@github-actions github-actions Bot mentioned this pull request May 4, 2026
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants