Files
b9f3eff2ec refactor(web): add codemod tool for feature architecture migration + migrate tx-notes (#7042)
* chore: add codemod tool for feature v3 architecture migration

Add a comprehensive two-phase codemod tool to automate migration of features
to the v3 architecture (lazy loading + feature handles).

Features:
- Phase 1 (Analyze): Scan feature and generate migration config
- Phase 2 (Execute): Apply transformations based on config
- Interactive CLI with dry-run support
- Automatic file reorganization and boilerplate generation
- JSCodeshift transforms for import updates

The tool automates ~50-70% of migration work:
- Creates contract.ts, feature.ts, index.ts
- Moves files to proper folder structure
- Updates import statements in consumers
- Adds TODO comments for manual steps

Remaining manual work:
- Complete useLoadFeature() migration
- Fix type errors
- Update tests

Location: tools/codemods/migrate-feature/
Usage: node tools/codemods/migrate-feature/dist/index.js --help

* feat(tx-notes): migrate to v3 feature architecture

Migrate tx-notes feature to v3 architecture using the codemod tool.

Changes:
- Created contract.ts, feature.ts, index.ts with flat structure
- Reorganized files into components/ and services/ folders
- Converted components to default exports
- Updated consumer files to use useLoadFeature()
- Fixed test imports

Migration tool handled:
- Boilerplate generation
- File reorganization
- Consumer file identification

Manual steps completed:
- Fixed component exports (named → default)
- Fixed internal component imports in TxNoteForm
- Updated 2 consumer files with useLoadFeature()
- Fixed test file import path
- Removed old index.tsx

Tests: ✅ All pass
Type-check: ✅ No errors
Feature status: ✅ Now using lazy loading + feature handles

* feat(codemod): add automatic export pattern conversion

Add automatic conversion of named exports to default exports for components.

This addresses the biggest manual pain point from the pilot migration.
Components often use named exports (export function/const) but the v3
architecture requires default exports for the contract pattern.

Changes:
- New transform: transforms/exports.ts
  - Detects named function exports: export function Name()
  - Detects named const exports: export const Name =
  - Converts to default exports automatically
- Integrated into execute.ts as Step 3 (after file moves, before boilerplate)
- Processes all component files in components/ folder
- Works for both function declarations and const arrow functions

Testing:
✅ export function Component() → export default function Component()
✅ export const Component = () => {} → const Component = ...; export default Component

Impact:
- Automation increases from ~60% to ~70%
- Eliminates most tedious manual conversion work
- Reduces migration time by ~10 minutes per feature

Updated manual steps documentation to reflect this automation.

* feat(codemod): add automatic relative import updates

Add automatic update of relative imports after file reorganization.

This addresses the second biggest manual pain point from the pilot migration.
When files are moved during reorganization, their internal imports break and
need manual path recalculation.

Changes:
- New transform: transforms/relativeImports.ts
  - Parses import statements in moved files
  - Detects relative imports (./file or ../folder/file)
  - Recalculates paths after file moves
  - Handles both default and named imports
  - Converts named imports to default when needed (after export conversion)
- Integrated into execute.ts as Step 3.5 (after export conversion)
- Processes all moved files automatically
- Reports detailed update information

Example transformation:
Before move: features/tx-notes/TxNoteForm.tsx
  import { TxNote } from './TxNote'
  import { TxNoteInput } from './TxNoteInput'

After move: features/tx-notes/components/TxNoteForm/index.tsx
  import TxNote from '../TxNote'          // ✅ Auto-updated path
  import TxNoteInput from '../TxNoteInput' // ✅ Auto-updated path
  // ✅ Auto-converted to default imports

Features:
- Handles default and named imports
- Handles type imports (import type)
- Updates paths for moved files
- Integrates with export conversion results
- Supports .ts, .tsx, .js, .jsx files

Impact:
- Automation increases from ~70% to ~80%
- Eliminates import path recalculation work
- Reduces migration time by ~5 minutes per feature

fix(tx-notes): optimize TxNote callback dependencies

Destructure feature exports to avoid adding entire feature object
to callback dependencies. This prevents unnecessary re-renders.

Before:
  const txNotes = useLoadFeature(TxNotesFeature)
  useCallback(..., [txNotes]) // ❌ Entire object

After:
  const { encodeTxNote, TxNoteForm } = useLoadFeature(TxNotesFeature)
  useCallback(..., [encodeTxNote]) // ✅ Only the function

* docs: fix feature architecture hooks pattern to avoid Rules of Hooks violations (#7044)

* docs: fix feature architecture hooks pattern to avoid Rules of Hooks violations

The previous architecture attempted to lazy-load hooks and stub them,
which violates React's Rules of Hooks. Dynamically swapping a stub
function for a real hook changes the number of internal hook calls
between renders.

Changes:
- Hooks are now exported directly from index.ts (always loaded, not lazy)
- Hooks are NOT included in feature contracts or feature.ts
- Updated useLoadFeature proxy to not stub hooks (returns undefined)
- Hooks kept lightweight with minimal imports
- Heavy logic moved to lazy-loaded services
- Updated all documentation and examples

This ensures hooks are always the same function reference and never
violate Rules of Hooks while keeping components and services lazy-loaded.

* docs: update walletconnect and counterfactual feature comments for new hooks pattern

Both features were already compliant with the new hooks architecture (hooks
not in contracts/feature.ts), but had outdated comments that needed updating.

Changes:
- Updated contract.ts comments to remove hook stub references
- Clarified that hooks are not in contracts (exported from index.ts if public)
- Updated index.ts usage examples to show hook imports
- Added notes about Rules of Hooks compliance

Both features correctly follow the pattern:
- WalletConnect: hooks used internally only, not exported
- Counterfactual: hooks exported from index.ts (always loaded)

* refactor(web): remove redundant feature component checks

Remove unnecessary conditional checks for WalletConnectWidget and CounterfactualStatusButton since useLoadFeature always returns an object with proxy-based stubs that handle loading/disabled states automatically.

---------

* refactor(codemod): update for new architecture - hooks are not lazy-loaded

BREAKING CHANGE: Hooks are now exported directly from index.ts instead of
being part of the lazy-loaded feature contract. This aligns with the updated
feature architecture that avoids Rules of Hooks violations.

Changes to codemod tool:
- templates.ts: Remove hooks from contract.ts generation
- templates.ts: Remove hooks from feature.ts generation
- templates.ts: Add direct hook exports in index.ts generation
- README.md: Update examples to show direct hook imports
- execute.ts: Update manual steps instructions

Changes to tx-notes (pilot feature):
- Update contract.ts comments (no hooks in contract)
- Update feature.ts comments (no hooks in lazy-loaded feature)
- Update index.ts comments and exports (hooks exported directly)

Rationale (from feature-architecture.md):
Lazy-loading hooks violates Rules of Hooks because the stub function has 0
internal hook calls while the real hook has N calls. Swapping between them
changes the number of hooks between renders, which React forbids.

The solution: Export hooks directly from index.ts (always loaded, not lazy).
Hooks should be kept lightweight with minimal imports. Heavy logic should be
in services (which are lazy-loaded).

Test results:
- Type-check: ✓ Clean
- Tests: ✓ All passing (tx-notes)
- Tool builds: ✓ No errors

* refactor(codemod): reduce complexity in analyze.ts and execute.ts

Addresses code health issues flagged in PR review (CodeScene analysis).

Changes to analyze.ts:
- Extract parseDefaultImport() helper function
- Extract parseNamedImports() helper function
- Simplify categorizeExports() using functional approach
- Reduces nesting depth from 4 to 2 in parseImports()
- Improves maintainability and readability

Changes to execute.ts:
- Extract executeFileReorganizationStep() for Step 2
- Extract executeExportConversionStep() for Step 3
- Extract executeImportUpdateStep() for Step 3.5
- Extract executeBoilerplateGenerationStep() for Step 4
- Extract executeConsumerUpdateStep() for Step 5
- Reduces cyclomatic complexity of executeMigration()
- Main function now 10 lines instead of ~240 lines
- Each step is now a focused, testable function

Changes to fileStructure.ts:
- Export FileMove interface for use in other modules
- Enables proper type safety across transforms

Impact:
- Code health improved (reduced "Bumpy Road Ahead" violations)
- Easier to understand and maintain
- Better testability (each step can be tested independently)
- No functional changes - behavior remains identical

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-01-27 21:56:04 +01:00

8.1 KiB

Feature Migration Codemod

A two-phase tool for migrating features to the v3 architecture (lazy loading + feature handles).

Overview

This tool helps automate the migration of features from the old architecture to the new v3 architecture documented in apps/web/docs/feature-architecture.md.

What it does

Phase 1 (Analyze):

  • Scans a feature directory
  • Discovers all exports (components, hooks, services)
  • Finds all consumer files
  • Analyzes current structure
  • Generates a migration config for review

Phase 2 (Execute):

  • Creates boilerplate files (contract.ts, feature.ts, index.ts)
  • Reorganizes file structure (moves files to appropriate folders)
  • Updates import statements in consumer files
  • Adds TODO comments for manual migration steps

What requires manual cleanup

  • Adjusting the public API in contract.ts (components and services only, NO hooks)
  • Completing the migration in consumer files:
    • Adding useLoadFeature() calls for components/services
    • Importing hooks directly from feature index
    • Converting component usage to feature.Component
    • Converting service usage to feature.service?.method() with $isReady checks
    • Removing unnecessary null checks for components
  • Keeping hooks lightweight (minimal imports, heavy logic in services)
  • Fixing type errors
  • Updating tests

Installation

From the repository root:

cd tools/codemods/migrate-feature
yarn install
yarn build

Usage

List all features

See which features are migrated and which aren't:

yarn migrate list

Phase 1: Analyze a feature

Interactive mode (recommended):

yarn migrate analyze --interactive

Analyze a specific feature:

yarn migrate analyze hypernative

This will:

  1. Scan the feature
  2. Show analysis results
  3. Prompt to save config to .codemod/{feature}.config.json

Phase 2: Execute migration

After reviewing the config, execute the migration:

yarn migrate execute hypernative

Or specify a custom config file:

yarn migrate execute --config .codemod/hypernative.config.json

Dry run (preview changes without modifying files):

yarn migrate execute hypernative --dry-run

Example Workflow

# 1. List features to see what needs migration
yarn migrate list

# 2. Analyze a feature
yarn migrate analyze hypernative

# 3. Review the generated config
cat .codemod/hypernative.config.json

# 4. Edit the config if needed (adjust public API, feature flag, etc.)
vim .codemod/hypernative.config.json

# 5. Execute migration (dry run first)
yarn migrate execute hypernative --dry-run

# 6. Execute for real
yarn migrate execute hypernative

# 7. Manual cleanup
# - Review generated files
# - Complete consumer file migrations
# - Fix type errors
# - Update tests

# 8. Verify
cd ../../..
yarn workspace @safe-global/web type-check
yarn workspace @safe-global/web lint
yarn workspace @safe-global/web test

Config File Format

The analysis phase generates a config file like this:

{
  "featureName": "hypernative",
  "featureFlag": "HYPERNATIVE",
  "publicAPI": {
    "components": ["HnMiniTxBanner", "HnQueueAssessmentBanner"],
    "hooks": ["useHnScanner", "useHnAssessment"],
    "services": ["hypernativeService"],
    "types": ["HnAssessment", "HnSeverity"],
    "constants": ["HN_API_URL"]
  },
  "structure": {
    "hasComponentsFolder": true,
    "hasHooksFolder": true,
    "hasServicesFolder": true,
    "hasStoreFolder": true,
    "hasUtilsFolder": false,
    "hasContextsFolder": false,
    "hasTypesFile": true,
    "hasConstantsFile": true,
    "hasReadme": true
  },
  "consumers": [
    {
      "filePath": "/path/to/consumer.tsx",
      "imports": [
        {
          "name": "HnMiniTxBanner",
          "type": "component",
          "importPath": "@/features/hypernative/components/HnMiniTxBanner",
          "isDefault": false,
          "isTypeOnly": false
        }
      ]
    }
  ]
}

You can edit this config before running the execute phase to:

  • Adjust which exports are public
  • Change the feature flag name
  • Skip certain files
  • Add custom notes

Manual Steps After Migration

After running the migration tool, you'll need to:

1. Review Generated Files

Check contract.ts, feature.ts, and index.ts to ensure they match your needs.

2. Complete Consumer Migrations

The tool adds TODO comments in consumer files. You need to:

// Before
import { MyComponent } from '@/features/myfeature/components/MyComponent'
import { useMyHook } from '@/features/myfeature/hooks/useMyHook'

function Consumer() {
  const data = useMyHook()
  return <MyComponent data={data} />
}

// After
import { MyFeature, useMyHook } from '@/features/myfeature'
import { useLoadFeature } from '@/features/__core__'

function Consumer() {
  const feature = useLoadFeature(MyFeature)

  // Hooks are imported directly (always loaded, not lazy)
  const data = useMyHook()

  // Components render via feature handle (lazy-loaded)
  // No null checks needed - proxy stubs handle it
  return <feature.MyComponent data={data} />
}

Important: Hooks are exported directly from index.ts (not lazy-loaded) to avoid Rules of Hooks violations. Services are accessed via the feature handle and should check $isReady before calling.

3. Fix Type Errors

Run type-check and fix any errors:

yarn workspace @safe-global/web type-check

4. Update Tests

Update test mocks to use the flat structure:

jest.mock('@/features/myfeature', () => ({
  MyFeature: {
    name: 'myfeature',
    useIsEnabled: () => true,
    load: () => Promise.resolve({
      default: {
        // Flat structure - components and services only (NO hooks)
        MyComponent: () => <div>Mock</div>,
        myService: jest.fn(),
      },
    }),
  },
  // Hooks are exported directly (always loaded, not in lazy-loaded feature)
  useMyHook: jest.fn(() => ({ data: 'mock' })),
}))

5. Run Tests

yarn workspace @safe-global/web test

6. Lint

yarn workspace @safe-global/web lint

Architecture Reference

For the complete architecture guide, see:

  • apps/web/docs/feature-architecture.md

Key principles:

  • Flat structure - no nested components/services in contract (hooks exported separately)
  • Hooks are NOT lazy-loaded - exported directly from index.ts to avoid Rules of Hooks violations
  • Proxy-based stubs - always returns an object for components/services, never null
  • Naming conventions - PascalCase (component), camelCase (service)
  • One dynamic import - feature.ts is lazy-loaded, use direct imports inside it (NO hooks in feature.ts)
  • typeof pattern - use typeof in contracts for IDE navigation

Troubleshooting

"Feature not found"

Make sure you're running the command from the repository root and the feature exists in apps/web/src/features/.

"Failed to transform imports"

The jscodeshift transform might fail on complex import patterns. You'll need to manually update those files.

Type errors after migration

This is expected. The tool generates boilerplate but you need to:

  1. Ensure all imports in feature.ts are correct
  2. Update consumer files to use the feature handle
  3. Fix any type mismatches

Development

To modify the tool:

  1. Edit TypeScript files in src/
  2. Rebuild: yarn build
  3. Test: yarn migrate --help

File Structure

migrate-feature/
├── src/
│   ├── index.ts           # CLI entry point
│   ├── types.ts           # Type definitions
│   ├── utils.ts           # Utility functions
│   ├── analyze.ts         # Phase 1: Analysis
│   ├── execute.ts         # Phase 2: Execution
│   ├── templates.ts       # Boilerplate generators
│   └── transforms/
│       ├── fileStructure.ts  # File reorganization
│       └── imports.ts        # Import updates
├── package.json
├── tsconfig.json
└── README.md

Contributing

When enhancing the tool:

  • Add new transforms to transforms/
  • Update templates in templates.ts
  • Add new CLI commands in index.ts
  • Update this README with new features