Files
zeekay 7200114387 Merge upstream/dev (52 commits) + sweep yarn → pnpm
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.
2026-05-27 03:25:16 -07:00

326 lines
10 KiB
TypeScript

/**
* Phase 2: Execute migration based on config
*/
import * as fs from 'fs'
import * as path from 'path'
import type { FeatureConfig, MigrationResult } from './types.js'
import { getFeaturePath, writeFile, formatTypeScript, findFiles } from './utils.js'
import { generateContractTemplate, generateFeatureTemplate, generateIndexTemplate } from './templates.js'
import {
ensureFolderStructure,
planFileReorganization,
executeFileReorganization,
type FileMove,
} from './transforms/fileStructure.js'
import { transformConsumerFile } from './transforms/imports.js'
import { convertExportsInFiles } from './transforms/exports.js'
import { updateImportsInMovedFiles } from './transforms/relativeImports.js'
/**
* Step 2: Handle file reorganization
*/
function executeFileReorganizationStep(config: FeatureConfig, result: MigrationResult, dryRun: boolean): FileMove[] {
console.log('šŸ“‹ Step 2: Planning file reorganization...')
const moves = planFileReorganization(config)
if (moves.length > 0) {
console.log(` Found ${moves.length} file(s) to reorganize`)
const moveResult = executeFileReorganization(moves, dryRun)
if (!moveResult.success) {
result.errors.push(...moveResult.errors)
result.success = false
} else {
result.filesMoved = moves.map((m) => m.to)
}
} else {
console.log(' No files need reorganization')
}
return moves
}
/**
* Step 3: Convert exports to default pattern
*/
function executeExportConversionStep(
featurePath: string,
result: MigrationResult,
dryRun: boolean,
): Map<string, string> {
console.log('šŸ”„ Step 3: Converting exports to default pattern...')
const componentFiles: string[] = []
const componentsPath = path.join(featurePath, 'components')
if (fs.existsSync(componentsPath)) {
const componentFolders = fs.readdirSync(componentsPath).filter((name) => {
const fullPath = path.join(componentsPath, name)
return fs.statSync(fullPath).isDirectory()
})
for (const folder of componentFolders) {
const indexFile = path.join(componentsPath, folder, 'index.tsx')
if (fs.existsSync(indexFile)) {
componentFiles.push(indexFile)
}
}
}
const componentExports = new Map<string, string>()
if (componentFiles.length > 0) {
const exportResult = convertExportsInFiles(componentFiles, dryRun)
if (!dryRun) {
exportResult.conversions.forEach((c) => {
componentExports.set(c.file, c.exportName)
})
}
if (dryRun) {
console.log(` Would convert ${exportResult.filesConverted} component(s) to default exports`)
} else {
if (exportResult.filesConverted > 0) {
console.log(` āœ“ Converted ${exportResult.filesConverted} component(s) to default exports`)
exportResult.conversions.forEach((c) => {
console.log(` - ${path.basename(path.dirname(c.file))}: ${c.exportName}`)
})
} else {
console.log(' No export conversions needed')
}
if (exportResult.errors.length > 0) {
result.errors.push(...exportResult.errors)
}
}
} else {
console.log(' No component files found')
}
return componentExports
}
/**
* Step 3.5: Update relative imports in moved files
*/
function executeImportUpdateStep(
moves: FileMove[],
componentExports: Map<string, string>,
result: MigrationResult,
dryRun: boolean,
): void {
if (moves.length === 0) return
console.log('šŸ”— Step 3.5: Updating relative imports...')
const importUpdateResult = updateImportsInMovedFiles(moves, componentExports, dryRun)
if (dryRun) {
if (importUpdateResult.filesUpdated > 0) {
console.log(
` Would update imports in ${importUpdateResult.filesUpdated} file(s) (${importUpdateResult.totalUpdates} import(s))`,
)
} else {
console.log(' No import updates needed')
}
} else {
if (importUpdateResult.filesUpdated > 0) {
console.log(
` āœ“ Updated imports in ${importUpdateResult.filesUpdated} file(s) (${importUpdateResult.totalUpdates} import(s))`,
)
importUpdateResult.details.forEach((d) => {
console.log(` - ${path.basename(path.dirname(d.file))}: ${d.updates} import(s)`)
})
} else {
console.log(' No import updates needed')
}
if (importUpdateResult.errors.length > 0) {
result.errors.push(...importUpdateResult.errors)
}
}
}
/**
* Step 4: Generate boilerplate files
*/
function executeBoilerplateGenerationStep(
config: FeatureConfig,
featurePath: string,
result: MigrationResult,
dryRun: boolean,
): void {
console.log('šŸ“ Step 4: Generating boilerplate files...')
// Generate contract.ts
const contractPath = path.join(featurePath, 'contract.ts')
if (!fs.existsSync(contractPath)) {
const contractContent = formatTypeScript(generateContractTemplate(config))
if (dryRun) {
console.log(` Would create: contract.ts`)
} else {
const success = writeFile(contractPath, contractContent)
if (success) {
result.filesCreated.push(contractPath)
console.log(' āœ“ Created contract.ts')
} else {
result.errors.push('Failed to create contract.ts')
result.success = false
}
}
} else {
result.warnings.push('contract.ts already exists, skipping')
}
// Generate feature.ts
const featureFilePath = path.join(featurePath, 'feature.ts')
if (!fs.existsSync(featureFilePath)) {
const featureContent = formatTypeScript(generateFeatureTemplate(config))
if (dryRun) {
console.log(` Would create: feature.ts`)
} else {
const success = writeFile(featureFilePath, featureContent)
if (success) {
result.filesCreated.push(featureFilePath)
console.log(' āœ“ Created feature.ts')
} else {
result.errors.push('Failed to create feature.ts')
result.success = false
}
}
} else {
result.warnings.push('feature.ts already exists, skipping')
}
// Generate index.ts
const indexPath = path.join(featurePath, 'index.ts')
if (!fs.existsSync(indexPath)) {
const indexContent = formatTypeScript(generateIndexTemplate(config))
if (dryRun) {
console.log(` Would create: index.ts`)
} else {
const success = writeFile(indexPath, indexContent)
if (success) {
result.filesCreated.push(indexPath)
console.log(' āœ“ Created index.ts')
} else {
result.errors.push('Failed to create index.ts')
result.success = false
}
}
} else {
result.warnings.push('index.ts already exists, skipping')
}
}
/**
* Step 5: Update consumer files
*/
function executeConsumerUpdateStep(config: FeatureConfig, result: MigrationResult, dryRun: boolean): void {
console.log('šŸ”„ Step 5: Updating consumer files...')
if (config.consumers.length === 0) {
console.log(' No consumers found')
return
}
console.log(` Found ${config.consumers.length} consumer file(s)`)
for (const consumer of config.consumers) {
const transformResult = transformConsumerFile(consumer.filePath, config.featureName, dryRun)
if (transformResult.success) {
result.filesModified.push(consumer.filePath)
} else {
result.errors.push(`Failed to transform ${consumer.filePath}: ${transformResult.error}`)
}
}
if (!dryRun) {
console.log(` āœ“ Updated ${result.filesModified.length} consumer file(s)`)
}
}
/**
* Execute the complete migration
*/
export async function executeMigration(config: FeatureConfig, dryRun: boolean = false): Promise<MigrationResult> {
const result: MigrationResult = {
success: true,
filesCreated: [],
filesModified: [],
filesMoved: [],
errors: [],
warnings: [],
}
const featurePath = getFeaturePath(config.featureName)
console.log(`\nšŸš€ Starting migration for feature: ${config.featureName}`)
console.log(dryRun ? ' (DRY RUN - no files will be modified)\n' : '')
// Step 1: Ensure folder structure
console.log('šŸ“ Step 1: Setting up folder structure...')
ensureFolderStructure(config, dryRun)
// Step 2: Handle file reorganization
const moves = executeFileReorganizationStep(config, result, dryRun)
// Step 3: Convert exports to default pattern
const componentExports = executeExportConversionStep(featurePath, result, dryRun)
// Step 3.5: Update relative imports
executeImportUpdateStep(moves, componentExports, result, dryRun)
// Step 4: Generate boilerplate files
executeBoilerplateGenerationStep(config, featurePath, result, dryRun)
// Step 5: Update consumer files
executeConsumerUpdateStep(config, result, dryRun)
// Summary
console.log('\n' + '='.repeat(60))
if (result.success) {
console.log('āœ… Migration completed successfully!')
} else {
console.log('āŒ Migration completed with errors')
}
console.log('\nSummary:')
console.log(` Files created: ${result.filesCreated.length}`)
console.log(` Files modified: ${result.filesModified.length}`)
console.log(` Files moved: ${result.filesMoved.length}`)
if (result.warnings.length > 0) {
console.log(`\nāš ļø Warnings:`)
result.warnings.forEach((w) => console.log(` - ${w}`))
}
if (result.errors.length > 0) {
console.log(`\nāŒ Errors:`)
result.errors.forEach((e) => console.log(` - ${e}`))
}
// Manual steps
console.log('\nšŸ“‹ Manual steps required:')
console.log(' 1. Review generated contract.ts and adjust public API as needed')
console.log(' 2. Review consumer files and complete the migration:')
console.log(' - Add: const feature = useLoadFeature(FeatureNameFeature)')
console.log(' - Replace: <Component /> → <feature.Component />')
console.log(' - Import hooks directly: import { useMyHook } from "@/features/feature-name"')
console.log(' - Replace: service.method() → feature.service?.method() (check $isReady for services)')
console.log(' 3. Keep hooks lightweight - move heavy imports to services if needed')
console.log(' 4. Update test file imports if needed')
console.log(' 5. Run: pnpm --filter @safe-global/web type-check')
console.log(' 6. Run: pnpm --filter @safe-global/web lint')
console.log(' 7. Run: pnpm --filter @safe-global/web test')
console.log('=' + '='.repeat(59))
return result
}