Upstream merge brings 52 commits from safe-global/safe-wallet-monorepo into the luxfi/safe-wallet@dev fork. Conflicts resolved: - yarn.lock: deleted (we are on pnpm) - apps/web/package.json: kept pnpm version + brand build scripts (build:safe / build:lux / build:hanzo) plus upstream's new generate-version.mjs step - apps/mobile/.../Signer/appKit.ts: took upstream's import pattern for shared/projectId, shared/metadata, shared/mmkvStorageAdapter Yarn sweep applied to 42 files (docs, github-actions, workflows, AGENTS, specs, codemods, scripts, READMEs). Net result: zero yarn-* command references anywhere outside node_modules. ❯ pnpm is the law in this fork. No yarn.
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
pnpm install
pnpm build
Usage
List all features
See which features are migrated and which aren't:
pnpm migrate list
Phase 1: Analyze a feature
Interactive mode (recommended):
pnpm migrate analyze --interactive
Analyze a specific feature:
pnpm 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:
pnpm migrate execute hypernative
Or specify a custom config file:
pnpm migrate execute --config .codemod/hypernative.config.json
Dry run (preview changes without modifying files):
pnpm migrate execute hypernative --dry-run
Example Workflow
# 1. List features to see what needs migration
pnpm migrate list
# 2. Analyze a feature
pnpm 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)
pnpm migrate execute hypernative --dry-run
# 6. Execute for real
pnpm migrate execute hypernative
# 7. Manual cleanup
# - Review generated files
# - Complete consumer file migrations
# - Fix type errors
# - Update tests
# 8. Verify
cd ../../..
pnpm --filter @safe-global/web type-check
pnpm --filter @safe-global/web lint
pnpm --filter @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:
pnpm --filter @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
pnpm --filter @safe-global/web test
6. Lint
pnpm --filter @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:
pnpm build - Test:
pnpm 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