* 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]>
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$isReadychecks - Removing unnecessary null checks for components
- Adding
- 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:
- Scan the feature
- Show analysis results
- 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/servicesin contract (hooks exported separately) - Hooks are NOT lazy-loaded - exported directly from
index.tsto 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.tsis lazy-loaded, use direct imports inside it (NO hooks in feature.ts) - typeof pattern - use
typeofin 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:
- Ensure all imports in
feature.tsare correct - Update consumer files to use the feature handle
- Fix any type mismatches
Development
To modify the tool:
- Edit TypeScript files in
src/ - Rebuild:
yarn build - 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