diff --git a/apps/vite-app/package.json b/apps/vite-app/package.json index 53b8ecbb5..cc9c5f37c 100644 --- a/apps/vite-app/package.json +++ b/apps/vite-app/package.json @@ -19,6 +19,7 @@ "react-dom": "19.1.2" }, "devDependencies": { + "@coinbase/cds-migrator": "workspace:^", "@types/react": "19.1.2", "@types/react-dom": "19.1.2", "@vitejs/plugin-react": "^5.1.2", diff --git a/packages/migrator/.gitignore b/packages/migrator/.gitignore new file mode 100644 index 000000000..42ff955ac --- /dev/null +++ b/packages/migrator/.gitignore @@ -0,0 +1,14 @@ +# Build outputs +esm/ +dts/ +*.tsbuildinfo + +# Node modules +node_modules/ + +# Migration history files (user-specific) +.cds-migration-history.json + +# Logs +*.log +migration.log diff --git a/packages/migrator/.npmignore b/packages/migrator/.npmignore new file mode 100644 index 000000000..a0af7f8f7 --- /dev/null +++ b/packages/migrator/.npmignore @@ -0,0 +1,12 @@ +src/ +tsconfig.json +tsconfig.build.json +babel.config.cjs +project.json +__tests__/ +__stories__/ +__mocks__/ +__fixtures__/ +*.test.* +*.spec.* +*.stories.* diff --git a/packages/migrator/README.md b/packages/migrator/README.md new file mode 100644 index 000000000..4de555224 --- /dev/null +++ b/packages/migrator/README.md @@ -0,0 +1,209 @@ +# @coinbase/cds-migrator + +Code migration tools for the Coinbase Design System. + +## Quick Start + +```bash +# Interactive mode (recommended) +npx @coinbase/cds-migrator + +# Non-interactive mode +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --dry-run +``` + +**β†’ See [Quick Start Guide](./docs/QUICK_START.md) for complete walkthrough.** + +## Features + +- 🎯 **Hierarchical Selection** - Choose category, then drill down to specific transforms +- πŸ“ **History Tracking** - Automatically prevents duplicate migrations +- πŸ” **Dry-Run Mode** - Preview all changes before applying +- πŸ“Š **Comprehensive Logging** - Track successes, warnings, and manual TODOs +- πŸ’¬ **TODO Comments** - Automatically marks code that needs manual review +- 🌳 **Configuration-Based** - Three-level structure (categories β†’ items β†’ transforms) +- πŸš€ **Convenience Commands** - `cds-migrate`, `cds-migrate:all`, `cds-migrate:clear-history` + +## Available Presets + +| Preset | Description | Status | +| ------------ | ---------------------------------------------------- | ------------ | +| **v8-to-v9** | Component API updates, hook changes, utility updates | βœ… Available | + +**Note:** Presets aren't just for version migrations - you can create presets for any collection of code transforms (deprecations, refactors, style updates, etc.). + +## How It Works + +The migrator uses a **simple 3-step selection**: + +1. **Choose a preset** (e.g., v8-to-v9) +2. **Choose a category** (or "All categories" to migrate everything) +3. **Choose a transform** in that category (or "All transforms" to run all) + +This hierarchical approach makes it easy to focus on one area at a time without being overwhelmed by choices. + +## Convenience Commands + +After installation, you get three commands: + +- `cds-migrate` - Full interactive CLI +- `cds-migrate:all -p ` - Quick migrate everything +- `cds-migrate:clear-history` - Quick clear history + +## Documentation + +### πŸ“˜ For Users + +Start here if you're using the migrator to upgrade your codebase: + +1. **[Quick Start Guide](./docs/QUICK_START.md)** ⭐ + - Get up and running in 5 minutes + - Your first migration walkthrough + - Common workflows and troubleshooting + +2. **[CLI Reference](./docs/CLI_REFERENCE.md)** ⭐ + - Interactive and non-interactive modes + - Selection options and "All" shortcuts + - CLI flags reference + - CI/CD integration examples + +3. **[Migration History](./docs/HISTORY.md)** + - How duplicate prevention works + - Understanding the history file + - Clearing history + - Team collaboration guidelines + +### πŸ”§ For Contributors + +Read these if you're adding new migrations or transforms: + +4. **[Configuration Guide](./docs/CONFIGURATION.md)** ⭐ + - Three-level structure explained + - Adding new migration versions + - Adding transforms to existing migrations + - Configuration best practices + +5. **[Writing Transforms](./docs/WRITING_TRANSFORMS.md)** ⭐ + - Creating jscodeshift codemods + - Common transformation patterns + - Testing transforms + - Best practices and anti-patterns + +6. **[Utilities API](./docs/UTILITIES.md)** + - Complete API reference + - TODO insertion functions + - Logging utilities + - Type definitions + +## Example Session + +``` +πŸš€ CDS Migrator + +? Which migration preset do you need? + ❯ v8 to v9 - Migrate from CDS v8 to v9 + +? Enter the path to your codebase: ./src + +? Which category of transforms do you want to run? + ❯ πŸ”˜ All categories + components - Component API changes + hooks - Hook API changes + utilities - Utility function changes + +Migration Plan: +================ +πŸ“¦ components: Component API changes + └─ Button (@coinbase/cds-web) + β€’ Rename 'variant' prop to 'appearance' + └─ Input (@coinbase/cds-web) + β€’ Update size prop values + +πŸ“¦ hooks: Hook API changes + └─ useTheme (@coinbase/cds-common) + β€’ Update destructured return values + +πŸ“¦ utilities: Utility function changes + └─ formatCurrency (@coinbase/cds-utils) + β€’ Update formatCurrency options parameter + +Total transforms: 4 + +? Run in dry-run mode? Yes + + β†’ Running transform: components.Button.button-variant-to-appearance + βœ“ Transform completed + + [... 3 more transforms ...] + +βœ… Migration completed successfully! +πŸ“ Migration log written to: migration.log +``` + +## Project Structure + +``` +packages/migrator/ +β”œβ”€β”€ docs/ # Documentation +β”‚ β”œβ”€β”€ QUICK_START.md # Getting started guide +β”‚ β”œβ”€β”€ CLI_REFERENCE.md # Interactive & non-interactive modes +β”‚ β”œβ”€β”€ CONFIGURATION.md # Config structure and setup +β”‚ β”œβ”€β”€ WRITING_TRANSFORMS.md # Transform development +β”‚ β”œβ”€β”€ UTILITIES.md # Utils API reference +β”‚ └── HISTORY.md # History tracking guide +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ cli.ts # Interactive CLI +β”‚ β”œβ”€β”€ runner.ts # Migration executor +β”‚ β”œβ”€β”€ types.ts # TypeScript types +β”‚ β”œβ”€β”€ utils/ # Shared utilities +β”‚ └── v8-to-v9/ # Version-specific migrations +β”‚ β”œβ”€β”€ config.json # Migration configuration +β”‚ └── transforms/ # jscodeshift codemods +└── package.json +``` + +## Development + +### Build & Test + +```bash +# Build the package +yarn nx run migrator:build + +# Type check +yarn nx run migrator:typecheck + +# Lint +yarn nx run migrator:lint + +# Test the CLI +cd packages/migrator +node esm/cli.js +``` + +### Adding New Presets + +See [docs/CONFIGURATION.md](./docs/CONFIGURATION.md) for complete instructions. + +Quick steps: + +1. Create `src/v9-to-v10/` directory +2. Add `config.json` with three-level structure +3. Create transforms in `transforms/` directory (as `.ts` files) +4. Update `AVAILABLE_PRESETS` in `src/cli.ts` + +## Contributing + +When creating transforms: + +- βœ… Write idempotent transforms (safe to run multiple times) +- βœ… Use the logging utilities to track changes +- βœ… Add TODO comments for complex cases +- βœ… Test in dry-run mode first +- βœ… Update the config.json + +See [docs/WRITING_TRANSFORMS.md](./docs/WRITING_TRANSFORMS.md) for detailed guide. + +## License + +See LICENSE in the root of the repository. diff --git a/packages/migrator/babel.config.cjs b/packages/migrator/babel.config.cjs new file mode 100644 index 000000000..bd2fee9a4 --- /dev/null +++ b/packages/migrator/babel.config.cjs @@ -0,0 +1,22 @@ +// @ts-check +const isTestEnv = process.env.NODE_ENV === 'test'; + +/** @type {import('@babel/core').TransformOptions} */ +module.exports = { + presets: [ + ['@babel/preset-env', { modules: isTestEnv ? 'commonjs' : false }], + ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', + ], + ignore: isTestEnv + ? [] + : [ + '**/__stories__/**', + '**/__tests__/**', + '**/__mocks__/**', + '**/__fixtures__/**', + '**/*.stories.*', + '**/*.test.*', + '**/*.spec.*', + ], +}; diff --git a/packages/migrator/docs/CLI_REFERENCE.md b/packages/migrator/docs/CLI_REFERENCE.md new file mode 100644 index 000000000..11e083897 --- /dev/null +++ b/packages/migrator/docs/CLI_REFERENCE.md @@ -0,0 +1,597 @@ +# CLI Reference + +Complete reference for using the CDS Migrator in both interactive and non-interactive modes. + +## Installation + +```bash +# Install as dev dependency (recommended) +yarn add -D @coinbase/cds-migrator + +# Or use npx without installing +npx @coinbase/cds-migrator +``` + +## Quick Reference + +### Interactive Mode + +```bash +# Using npx (no installation needed) +npx @coinbase/cds-migrator + +# Or if installed locally +yarn cds-migrate +``` + +### Convenience Commands (after installation) + +Quick commands for common operations: + +```bash +# Migrate everything (shortcut) +yarn cds-migrate:all -p v8-to-v9 --path ./src --dry-run + +# Clear history (shortcut) +yarn cds-migrate:clear-history --path ./src +``` + +### Full CLI Flags + +```bash +# Migrate everything +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all + +# Dry-run (preview changes) +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --dry-run + +# Migrate specific category +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --category components + +# Migrate specific items +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --item components.Button + +# Migrate specific transform +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --transform components.Button.button-variant-to-appearance + +# Clear history +npx @coinbase/cds-migrator --clear-history --path ./src +``` + +### Optional: Add Scripts to package.json + +For convenience, add these to your project's package.json: + +```json +{ + "scripts": { + "migrate": "cds-migrate", + "migrate:dry": "cds-migrate -p v8-to-v9 --path ./src --all --dry-run", + "migrate:apply": "cds-migrate -p v8-to-v9 --path ./src --all" + } +} +``` + +Then run: + +```bash +yarn migrate # Interactive +yarn migrate:dry # Preview changes +yarn migrate:apply # Apply changes +``` + +--- + +## Part 1: Interactive Mode + +### Selection Flow + +The migrator uses a **hierarchical selection** where you choose categories first, then transforms within each category: + +``` +Step 1: Which preset? + β†’ v8-to-v9 + +Step 2: Enter path + β†’ ./src + +Step 3: [Shows history if exists] + +Step 4: Which categories? + β”œβ”€ πŸ”˜ All categories β†’ DONE (runs everything) + └─ Select specific β†’ Continue to Step 5 + +Step 5: For EACH selected category, which transforms? + β”œβ”€ πŸ”˜ All transforms in [category] β†’ Runs all in that category + └─ Select specific β†’ Runs only those selected + +Step 6: Dry-run mode? + +Step 7: Execute +``` + +### Example Flows + +**Flow 1: Migrate Everything** + +``` +? Which categories? β†’ πŸ”˜ All categories +βœ“ Done! Runs all 4 transforms +``` + +**Flow 2: All Components** + +``` +? Which categories? β†’ components +? Transforms in components? β†’ πŸ”˜ All transforms in components +βœ“ Runs all component transforms (Button + Input) +``` + +**Flow 3: Just One Transform** + +``` +? Which categories? β†’ components +? Transforms in components? β†’ Button.button-variant-to-appearance +βœ“ Runs only that one transform +``` + +**Flow 4: Multiple Categories, All Transforms** + +``` +? Which categories? β†’ components, hooks +? Transforms in components? β†’ πŸ”˜ All transforms in components +? Transforms in hooks? β†’ πŸ”˜ All transforms in hooks +βœ“ Runs all transforms in both categories (3 transforms) +``` + +**Flow 5: Cherry-Pick from Multiple Categories** + +``` +? Which categories? β†’ components, hooks +? Transforms in components? β†’ Button.button-variant-to-appearance +? Transforms in hooks? β†’ useTheme.use-theme-return-type +βœ“ Runs 2 specific transforms +``` + +### What Gets Run + +| Selection | Result | +| --------------------------------------------- | ---------------------------------- | +| πŸ”˜ All categories | All transforms from all categories | +| categories: [components] + πŸ”˜ All transforms | All transforms in components | +| categories: [components] + specific | Only selected component transforms | +| categories: [components, hooks] + all in each | All transforms in both categories | + +--- + +## Part 2: Non-Interactive Mode (CLI Flags) + +Use CLI flags to run without prompts - perfect for CI/CD and automation. + +### Required Flags + +For non-interactive mode, you must provide: + +- `-p, --preset ` - Migration preset (e.g., v8-to-v9) +- `--path ` - Target path (default: ./src) +- One selection: `--all`, `--category`, `--item`, or `--transform` + +### Selection Flags + +Each selection level runs progressively more transforms: + +| Flag | What It Runs | Example | +| ------------- | ----------------------- | -------------------------------------------- | +| `--all` | Everything | All categories β†’ all items β†’ all transforms | +| `--category` | All items in category | All transforms for selected category's items | +| `--item` | All transforms for item | All transforms for the selected item | +| `--transform` | Specific transform only | Just that one transform | + +#### `--all` + +Migrate everything (all categories, items, and transforms). + +```bash +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all +``` + +#### `--category ` + +Migrate specific categories (can specify multiple). + +**This runs ALL items and transforms in the selected categories.** + +```bash +# Single category (runs all items and transforms in components) +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --category components + +# Multiple categories +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --category components hooks +``` + +**Example:** `--category components` will run all transforms for Button, Input, and any other components. + +#### `--item ` + +Migrate specific items using dot notation: `category.item` + +**This runs ALL transforms for the selected items.** + +```bash +# Single item (runs all transforms for Button) +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --item components.Button + +# Multiple items +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --item components.Button components.Input hooks.useTheme +``` + +**Why dot notation?** Items can have the same name in different categories, so `category.item` ensures we select the right one. + +#### `--transform ` + +Migrate specific transforms using full dot notation: `category.item.transform-name` + +```bash +# Single transform (most granular control) +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --transform components.Button.button-variant-to-appearance + +# Multiple transforms +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src \ + --transform components.Button.button-variant-to-appearance \ + --transform components.Input.input-size-values +``` + +**Why full path?** Transform names might be reused across items, so `category.item.transform` is the unique identifier. + +### Mode Flags + +#### `-d, --dry-run` + +Preview changes without modifying files. + +```bash +# Dry-run (safe to test) +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --dry-run + +# Apply changes (no flag) +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all +``` + +#### `--skip-confirmation` + +Skip confirmation prompts (useful for automation). + +```bash +# For CI/CD +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --skip-confirmation +``` + +**⚠️ Use with caution** - bypasses all safety prompts! + +### Utility Flags + +#### `--clear-history` + +Clear migration history for a path. + +```bash +# With confirmation +npx @coinbase/cds-migrator --clear-history --path ./src + +# Skip confirmation +npx @coinbase/cds-migrator --clear-history --path ./src --skip-confirmation +``` + +**Note:** Only requires `--path`, no preset needed. + +#### `-h, --help` + +Show help message with all flags. + +```bash +npx @coinbase/cds-migrator --help +``` + +#### `-V, --version` + +Show package version. + +```bash +npx @coinbase/cds-migrator --version +``` + +### Flags Summary Table + +| Flag | Short | Description | Example | +| --------------------- | ----- | ------------------- | --------------------------------------- | +| `--preset` | `-p` | Migration preset | `-p v8-to-v9` | +| `--path` | | Target path | `--path ./src` | +| `--dry-run` | `-d` | Preview mode | `-d` | +| `--skip-confirmation` | | Skip prompts | `--skip-confirmation` | +| `--clear-history` | | Clear history | `--clear-history` | +| `--all` | | Migrate everything | `--all` | +| `--category` | | Specific categories | `--category components` | +| `--item` | | Specific items | `--item components.Button` | +| `--transform` | | Specific transforms | `--transform components.Button.variant` | +| `--help` | `-h` | Show help | `-h` | +| `--version` | `-V` | Show version | `-V` | + +--- + +## Part 3: Common Workflows + +### Local Development + +```bash +# Interactive (recommended) +npx @coinbase/cds-migrator + +# Quick dry-run +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --dry-run +``` + +### CI/CD Pipeline + +```bash +#!/bin/bash +# migrate.sh + +# Preview first +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --dry-run --skip-confirmation + +# Apply if preview succeeded +if [ $? -eq 0 ]; then + npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --skip-confirmation +fi +``` + +### GitHub Actions + +```yaml +name: CDS Migration +on: workflow_dispatch + +jobs: + migrate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Run migration + run: | + npx @coinbase/cds-migrator \ + --preset v8-to-v9 \ + --path ./src \ + --all \ + --skip-confirmation + + - name: Upload log + uses: actions/upload-artifact@v3 + with: + name: migration-log + path: migration.log +``` + +### Incremental Migration + +```bash +# Day 1: Components +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --category components + +# Day 2: Hooks +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --category hooks + +# Day 3: Everything else +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all +``` + +History tracking ensures no duplicates across runs. + +### Package.json Scripts + +Add migration scripts to your project for convenience: + +```json +{ + "scripts": { + "migrate": "cds-migrate", + "migrate:dry": "cds-migrate -p v8-to-v9 --path ./src --all --dry-run", + "migrate:apply": "cds-migrate -p v8-to-v9 --path ./src --all --skip-confirmation", + "migrate:components": "cds-migrate -p v8-to-v9 --path ./src --category components" + } +} +``` + +Then use them: + +```bash +yarn migrate # Interactive mode +yarn migrate:dry # Preview all changes +yarn migrate:apply # Apply all changes +yarn migrate:components # Migrate only components +``` + +--- + +## Part 4: Interactive vs Non-Interactive + +### When to Use Interactive Mode + +βœ… **Use interactive when:** + +- First-time migration +- Want to explore options +- Not sure what to migrate +- Manual review needed + +```bash +npx @coinbase/cds-migrator +# Prompts guide you through +``` + +### When to Use Non-Interactive Mode + +βœ… **Use non-interactive when:** + +- CI/CD pipelines +- Automated scripts +- You know exactly what to migrate +- Repeated operations + +```bash +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --skip-confirmation +``` + +### Switching Between Modes + +The tool automatically detects which mode based on provided flags: + +**Triggers Interactive:** + +```bash +npx @coinbase/cds-migrator # No flags +npx @coinbase/cds-migrator -p v8-to-v9 # Missing selection +npx @coinbase/cds-migrator --path ./src --all # Missing preset +``` + +**Triggers Non-Interactive:** + +```bash +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all # All required flags +``` + +--- + +## Part 5: Examples by Use Case + +### Complete Migration + +```bash +# Step 1: Preview everything +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --dry-run + +# Step 2: Review migration.log +cat migration.log + +# Step 3: Apply changes +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all +``` + +### Selective Migration + +```bash +# Just components category +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --category components + +# Just Button item (all its transforms) +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --item components.Button + +# Just one specific transform +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src \ + --transform components.Button.button-variant-to-appearance +``` + +### Testing During Development + +```bash +# Test on sample directory +npx @coinbase/cds-migrator -p v8-to-v9 --path ./test-samples --all + +# Clear and re-test +npx @coinbase/cds-migrator --clear-history --path ./test-samples --skip-confirmation +npx @coinbase/cds-migrator -p v8-to-v9 --path ./test-samples --all +``` + +### Automated Daily Migration + +```bash +#!/bin/bash +# daily-migrate.sh + +DATE=$(date +%Y-%m-%d) +LOG_DIR="./migration-logs" + +mkdir -p "$LOG_DIR" + +npx @coinbase/cds-migrator \ + -p v8-to-v9 \ + --path ./src \ + --all \ + --skip-confirmation \ + && mv migration.log "$LOG_DIR/migration-$DATE.log" +``` + +--- + +## Troubleshooting + +### "Error: Must specify --all, --category, --item, or --transform" + +You need to specify what to migrate: + +```bash +# ❌ Missing selection +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src + +# βœ… With selection +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all +``` + +### "Warning: Some transforms have already been run" + +Options: + +1. Use `--skip-confirmation` (automatically skips duplicates) +2. Use `--clear-history` first to reset +3. Switch to interactive mode to choose options + +```bash +# Skip duplicate warning +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --skip-confirmation +``` + +### Invalid Preset + +Check available presets: + +```bash +npx @coinbase/cds-migrator --help +``` + +Currently available: `v8-to-v9` + +--- + +## Exit Codes + +| Code | Meaning | +| ---- | -------------------------------------------------------------- | +| `0` | Success | +| `1` | Error (migration failed, validation failed, or user cancelled) | + +**Example:** + +```bash +npx @coinbase/cds-migrator -p v8-to-v9 --path ./src --all --skip-confirmation +if [ $? -eq 0 ]; then + echo "βœ… Migration succeeded" +else + echo "❌ Migration failed" + exit 1 +fi +``` + +--- + +## See Also + +- [Quick Start](./QUICK_START.md) - Your first migration +- [Configuration](./CONFIGURATION.md) - Understanding the structure +- [History](./HISTORY.md) - How tracking works +- [Writing Transforms](./WRITING_TRANSFORMS.md) - Creating codemods +- [Utilities API](./UTILITIES.md) - Helper functions diff --git a/packages/migrator/docs/CONFIGURATION.md b/packages/migrator/docs/CONFIGURATION.md new file mode 100644 index 000000000..eefdd8072 --- /dev/null +++ b/packages/migrator/docs/CONFIGURATION.md @@ -0,0 +1,614 @@ +# Migration Configuration Guide + +How to structure and configure migrations using the three-level hierarchy. + +## Overview + +Each major preset (e.g., v8-to-v9) has a `config.json` file that defines: + +1. **Categories** - High-level groupings (components, hooks, utilities) +2. **Variables** - Specific exports (Button, useTheme, formatCurrency) +3. **Transforms** - Individual codemods to run + +This structure allows users to migrate everything at once or pick exactly what they need. + +## Configuration Structure + +``` +Categories (components, hooks, utilities) + └─ Variables (Button, Input, useTheme, etc.) + └─ Transforms (specific codemods) +``` + +### Complete Example + +```json +{ + "version": "v8-to-v9", + "description": "Migration from CDS v8 to v9", + "categories": { + "components": { + "description": "Component API changes", + "variables": { + "Button": { + "description": "Button component prop changes", + "package": "@coinbase/cds-web", + "transforms": [ + { + "name": "button-variant-to-appearance", + "description": "Rename 'variant' prop to 'appearance'", + "file": "transforms/button-variant-to-appearance.ts" + }, + { + "name": "button-remove-deprecated-sizes", + "description": "Remove deprecated size values", + "file": "transforms/button-remove-deprecated-sizes.ts" + } + ] + }, + "Input": { + "description": "Input component prop changes", + "package": "@coinbase/cds-web", + "transforms": [ + { + "name": "input-size-values", + "description": "Update size prop values", + "file": "transforms/input-size-values.ts" + } + ] + } + } + }, + "hooks": { + "description": "Hook API changes", + "variables": { + "useTheme": { + "description": "useTheme hook signature changes", + "package": "@coinbase/cds-common", + "transforms": [ + { + "name": "use-theme-return-type", + "description": "Update destructured return values", + "file": "transforms/use-theme-return-type.ts" + } + ] + } + } + } + } +} +``` + +## Level Definitions + +### Level 1: Categories + +Organize changes by type: + +| Category | Description | Examples | +| ------------ | ------------------------ | ------------------------------ | +| `components` | React component changes | Button, Input, Modal | +| `hooks` | Hook API changes | useTheme, useMediaQuery | +| `utilities` | Utility function changes | formatCurrency, parseDate | +| `types` | TypeScript type changes | Props interfaces, type aliases | +| `constants` | Constant value changes | Theme tokens, breakpoints | +| `styles` | Style/CSS changes | Class names, CSS variables | + +**Category Schema:** + +```typescript +{ + description: string; // Human-readable explanation + variables: { // Map of exported symbols + [name: string]: MigrationVariable; + } +} +``` + +### Level 2: Variables + +Specific exports from CDS packages: + +**Variable Schema:** + +```typescript +{ + description: string; // What changed about this export + package: string; // Which package exports it + transforms: Transform[]; // Array of codemods +} +``` + +**Examples:** + +- `Button` from `@coinbase/cds-web` +- `useTheme` from `@coinbase/cds-common` +- `formatCurrency` from `@coinbase/cds-utils` + +**Naming conventions:** + +- Use the actual export name (PascalCase for components/types, camelCase for functions/hooks) +- Match what users import: `import { Button } from '@coinbase/cds-web'` + +### Level 3: Transforms + +Individual jscodeshift codemods: + +**Transform Schema:** + +```typescript +{ + name: string; // Unique identifier (kebab-case) + description: string; // What this transform does + file: string; // Path relative to version directory + extensions?: string; // Optional: "tsx,ts,jsx,js" (default) +} +``` + +**Naming conventions:** + +- Use kebab-case: `button-variant-to-appearance` +- Be descriptive: `{component}-{what-it-does}` +- Examples: `input-size-values`, `use-theme-return-type` + +## Adding a New Migration Version + +### 1. Create Directory Structure + +```bash +mkdir -p packages/migrator/src/v9-to-v10/transforms +``` + +### 2. Create config.json + +```json +{ + "version": "v9-to-v10", + "description": "Migration from CDS v9 to v10", + "categories": { + "components": { + "description": "Component API changes", + "variables": {} + } + } +} +``` + +### 3. Create index.ts + +```typescript +/** + * v9 to v10 Migration + */ + +export const description = ` +CDS v9 to v10 Migration +====================== + +Breaking changes in v10: +- [List breaking changes here] + +Please review the changes carefully before committing. +`; +``` + +### 4. Update CLI + +In `src/cli.ts`, add to `SUPPORTED_MIGRATIONS`: + +```typescript +const SUPPORTED_MIGRATIONS = [ + { + name: 'v8 to v9', + value: 'v8-to-v9', + description: 'Migrate from CDS v8 to v9', + }, + { + name: 'v9 to v10', // Add this + value: 'v9-to-v10', + description: 'Migrate from CDS v9 to v10', + }, +] as const; +``` + +## Adding Transforms to Existing Migration + +### 1. Create the Transform File + +```typescript +// src/v8-to-v9/transforms/button-variant-to-appearance.ts +import type { API, FileInfo } from 'jscodeshift'; +import { getLogger } from '../../utils'; + +export default function transformer(file: FileInfo, api: API) { + const j = api.jscodeshift; + const root = j(file.source); + const logger = getLogger(); + + let hasChanges = false; + + // Transform logic here + root + .find(j.JSXAttribute, { name: { name: 'variant' } }) + .filter((path) => { + const jsxElement = path.parent.value; + return ( + j.JSXIdentifier.check(jsxElement.openingElement.name) && + jsxElement.openingElement.name.name === 'Button' + ); + }) + .forEach((path) => { + if (j.JSXIdentifier.check(path.value.name)) { + path.value.name.name = 'appearance'; + hasChanges = true; + logger?.success('Renamed variant to appearance', file.path, path.value.loc?.start.line); + } + }); + + return hasChanges ? root.toSource() : null; +} +``` + +### 2. Add to config.json + +```json +{ + "categories": { + "components": { + "variables": { + "Button": { + "description": "Button component prop changes", + "package": "@coinbase/cds-web", + "transforms": [ + { + "name": "button-variant-to-appearance", + "description": "Rename 'variant' prop to 'appearance'", + "file": "transforms/button-variant-to-appearance.ts" + } + ] + } + } + } + } +} +``` + +### 3. Test + +```bash +# Build +yarn nx run migrator:build + +# Test in dry-run +cd packages/migrator +node esm/cli.js +# Select: By specific transform β†’ components.Button.button-variant-to-appearance +``` + +## Configuration Best Practices + +### 1. Granular Transforms + +One transform should do one thing: + +βœ… **Good:** + +```json +{ + "transforms": [ + { + "name": "button-variant-to-appearance", + "description": "Rename 'variant' prop to 'appearance'" + }, + { + "name": "button-remove-deprecated-sizes", + "description": "Remove deprecated size values" + } + ] +} +``` + +❌ **Bad:** + +```json +{ + "transforms": [ + { + "name": "button-all-changes", + "description": "Update all Button props" + } + ] +} +``` + +**Why:** Granular transforms give users more control and make debugging easier. + +### 2. Clear Descriptions + +Descriptions should explain what changes, not implementation details: + +βœ… **Good:** + +```json +"description": "Rename 'variant' prop to 'appearance'" +``` + +❌ **Bad:** + +```json +"description": "Run jscodeshift transform to update JSX attributes" +``` + +### 3. Logical Grouping + +Group related transforms under the same variable: + +βœ… **Good:** + +```json +"Button": { + "transforms": [ + { "name": "button-variant-to-appearance" }, + { "name": "button-size-values" }, + { "name": "button-remove-deprecated-props" } + ] +} +``` + +❌ **Bad:** + +```json +"Button": { + "transforms": [{ "name": "button-variant-to-appearance" }] +}, +"ButtonSize": { + "transforms": [{ "name": "button-size-values" }] +} +``` + +### 4. Package Attribution + +Always specify which package exports the variable: + +```json +{ + "Button": { + "package": "@coinbase/cds-web", // ← Required! + "transforms": [...] + } +} +``` + +This helps users understand where imports come from. + +### 5. Meaningful Categories + +Use categories that match how developers think: + +βœ… **Good Categories:** + +- `components` - React components +- `hooks` - React hooks +- `utilities` - Helper functions +- `types` - TypeScript types + +❌ **Avoid:** + +- `breaking-changes` - Too vague +- `misc` - Not organized +- `v9-updates` - Redundant with version + +## Migration Execution Flow + +Understanding how the config is used: + +``` +1. User selects migration version (v8-to-v9) + ↓ +2. Load config.json from src/v8-to-v9/ + ↓ +3. User chooses scope (all, category, variable, or transform) + ↓ +4. Build list of transforms based on selection + ↓ +5. Show migration plan + ↓ +6. Execute transforms in order + ↓ +7. Record in migration history +``` + +## Example Migration Plans + +### Selecting "Everything" + +``` +Migration Plan: +================ + +πŸ“¦ components: Component API changes + └─ Button (@coinbase/cds-web) + β€’ Rename 'variant' prop to 'appearance' + β€’ Remove deprecated size values + └─ Input (@coinbase/cds-web) + β€’ Update size prop values + +πŸ“¦ hooks: Hook API changes + └─ useTheme (@coinbase/cds-common) + β€’ Update destructured return values + +Total transforms: 4 +``` + +### Selecting "By Variable" + +``` +? Select items to migrate: + β—‰ components.Button + β—― components.Input + β—‰ hooks.useTheme + +Migration Plan: +================ + +πŸ“¦ components: Component API changes + └─ Button (@coinbase/cds-web) + β€’ Rename 'variant' prop to 'appearance' + β€’ Remove deprecated size values + +πŸ“¦ hooks: Hook API changes + └─ useTheme (@coinbase/cds-common) + β€’ Update destructured return values + +Total transforms: 3 +``` + +## Advanced Configuration + +### Custom File Extensions + +For transforms that should only run on specific file types: + +```json +{ + "name": "style-object-to-stylesheet", + "description": "Convert inline styles to StyleSheet", + "file": "transforms/style-object-to-stylesheet.ts", + "extensions": "tsx,jsx" // Only run on TSX/JSX files +} +``` + +### Transform Ordering + +Transforms run in the order they appear in the config: + +```json +{ + "transforms": [ + { + "name": "remove-old-imports", + "description": "Remove old imports" + }, + { + "name": "add-new-imports", + "description": "Add new imports" + } + ] +} +``` + +**Important:** The order matters if transforms depend on each other! + +## Schema Reference + +### MigrationConfig + +```typescript +interface MigrationConfig { + version: string; // "v8-to-v9" + description: string; // Overall description + categories: Record; // Category map +} +``` + +### MigrationCategory + +```typescript +interface MigrationCategory { + description: string; // Category description + variables: Record; // Variable map +} +``` + +### MigrationVariable + +```typescript +interface MigrationVariable { + description: string; // What changed + package: string; // Source package + transforms: Transform[]; // List of transforms +} +``` + +### Transform + +```typescript +interface Transform { + name: string; // Unique ID (kebab-case) + description: string; // Human-readable explanation + file: string; // Path relative to version dir + extensions?: string; // Optional: file extensions +} +``` + +## Validation + +The config loader will validate: + +βœ… config.json exists +βœ… Valid JSON format +βœ… All referenced transform files exist +βœ… No duplicate transform names within a variable + +## Tips for Large Migrations + +### Split by Package + +Organize by which CDS package is affected: + +```json +{ + "categories": { + "cds-web": { + "description": "@coinbase/cds-web changes", + "variables": { + /* web components */ + } + }, + "cds-mobile": { + "description": "@coinbase/cds-mobile changes", + "variables": { + /* mobile components */ + } + }, + "cds-common": { + "description": "@coinbase/cds-common changes", + "variables": { + /* hooks, utilities */ + } + } + } +} +``` + +### Use Descriptive Transform Names + +Make it easy to understand what each transform does: + +βœ… Good: + +- `button-variant-to-appearance` +- `input-remove-error-text-prop` +- `modal-overlay-click-behavior` + +❌ Avoid: + +- `transform1` +- `fix-button` +- `update` + +### Document Breaking Changes + +Use the description field effectively: + +```json +{ + "description": "Migration from CDS v8 to v9\n\nBreaking changes:\n- Button 'variant' prop renamed to 'appearance'\n- Input 'errorText' prop removed\n- useTheme returns new structure" +} +``` + +## See Also + +- [Writing Transforms](./WRITING_TRANSFORMS.md) - Create the transform files +- [Utilities](./UTILITIES.md) - Use shared utilities in transforms +- [Quick Start](./QUICK_START.md) - Run your first migration diff --git a/packages/migrator/docs/HISTORY.md b/packages/migrator/docs/HISTORY.md new file mode 100644 index 000000000..4583cbd92 --- /dev/null +++ b/packages/migrator/docs/HISTORY.md @@ -0,0 +1,432 @@ +# Migration History Tracking + +The CDS Migrator automatically tracks which transforms have been run on each directory to prevent accidental duplicate migrations. + +## How It Works + +### Automatic Tracking + +When you run a migration (in non-dry-run mode), the migrator: + +1. **Records the transform** - Saves which transform was run, when, and for which version +2. **Creates a history file** - Stores this in `.cds-migration-history.json` in the target directory +3. **Checks before running** - Before executing any transform, checks if it's already been run + +### History File + +The `.cds-migration-history.json` file is created in your target directory: + +```json +{ + "targetPath": "./src", + "entries": [ + { + "transformId": "components.Button.button-variant-to-appearance", + "timestamp": "2026-02-11T12:00:00.000Z", + "version": "v8-to-v9", + "dryRun": false + } + ], + "lastUpdated": "2026-02-11T12:00:00.000Z" +} +``` + +**Important Notes:** + +- This file should be **committed to version control** +- It helps team members avoid re-running the same migrations +- Dry-run mode does NOT create or update the history file + +## User Experience + +### First Run + +When running a migration for the first time: + +```bash +? What would you like to migrate? + ❯ Everything (all changes) + +Migration Plan: +================ +πŸ“¦ components: Component API changes + └─ Button (@coinbase/cds-web) + β€’ Rename 'variant' prop to 'appearance' + +Total transforms: 1 + +? Enter the path to your codebase: ./src +? Run in dry-run mode? No + + β†’ Running transform: components.Button.button-variant-to-appearance + Rename 'variant' prop to 'appearance' + + βœ“ Transform completed: button-variant-to-appearance + +βœ… Migration completed successfully! +``` + +### Attempting to Re-run + +If you try to run the same migration again: + +```bash +Migration Plan: +================ +πŸ“¦ components: Component API changes + └─ Button (@coinbase/cds-web) + β€’ Rename 'variant' prop to 'appearance' + +Total transforms: 1 + +? Enter the path to your codebase: ./src +? Run in dry-run mode? No + +⚠️ Warning: Some transforms have already been run on this path: + + β€’ components.Button.button-variant-to-appearance + +πŸ“œ Migration History +================== + +v8-to-v9: + β€’ components.Button.button-variant-to-appearance (2/11/2026) + +Last updated: 2/11/2026, 12:00:00 PM + +? How would you like to proceed? + ❯ Skip already-run transforms (recommended) + Re-run all transforms (may cause issues) + Cancel migration +``` + +### Options When Duplicate Detected + +1. **Skip already-run transforms (recommended)** + - Only runs transforms that haven't been executed before + - Safest option to prevent issues + - The runner will automatically skip completed transforms + +2. **Re-run all transforms (may cause issues)** + - Forces re-execution of all selected transforms + - May cause problems if transforms aren't idempotent + - Use with caution + +3. **Cancel migration** + - Exits without making any changes + +### During Execution + +When transforms are skipped: + +```bash + β†’ Running transform: components.Input.input-size-values + Update size prop values (sm/md/lg to small/medium/large) + + βœ“ Transform completed: input-size-values + + ⊘ Skipping (already run): components.Button.button-variant-to-appearance + +⊘ Skipped 1 transform(s) that were already run. + +βœ… Migration completed successfully! +``` + +## API for Programmatic Use + +### Load History + +```typescript +import { loadMigrationHistory } from '@coinbase/cds-migrator'; + +const history = loadMigrationHistory('./src'); +if (history) { + console.log(`Last updated: ${history.lastUpdated}`); + console.log(`Total migrations: ${history.entries.length}`); +} +``` + +### Check if Transform Was Run + +```typescript +import { hasTransformBeenRun } from '@coinbase/cds-migrator'; + +const wasRun = hasTransformBeenRun('./src', 'components.Button.button-variant-to-appearance'); + +if (wasRun) { + console.log('This transform has already been applied'); +} +``` + +### Get Already-Run Transforms + +```typescript +import { getAlreadyRunTransforms } from '@coinbase/cds-migrator'; + +const transformsToRun = [ + 'components.Button.button-variant-to-appearance', + 'components.Input.input-size-values', +]; + +const alreadyRun = getAlreadyRunTransforms('./src', transformsToRun); +console.log('Already run:', alreadyRun); +``` + +### View History Summary + +```typescript +import { buildHistorySummary } from '@coinbase/cds-migrator'; + +const summary = buildHistorySummary('./src'); +console.log(summary); +// Outputs a formatted summary of all migrations +``` + +### Clear History + +**CLI (recommended):** + +```bash +# Clear history for a path +npx @coinbase/cds-migrator --clear-history -p ./src + +# Skip confirmation prompt +npx @coinbase/cds-migrator --clear-history -p ./src --skip-confirmation +``` + +**Programmatically:** + +```typescript +import { clearMigrationHistory } from '@coinbase/cds-migrator'; + +// Use with caution - this removes all migration tracking +clearMigrationHistory('./src'); +``` + +## Best Practices + +### βœ… Do + +- **Commit the history file** to version control +- **Review the history** before running migrations +- **Use dry-run mode** first to preview changes +- **Skip already-run transforms** when prompted (recommended option) + +### ❌ Don't + +- Don't delete `.cds-migration-history.json` without good reason +- Don't re-run transforms unless absolutely necessary +- Don't ignore warnings about duplicate runs +- Don't add `.cds-migration-history.json` to `.gitignore` + +## Team Collaboration + +### Scenario: Multiple Team Members + +1. **Developer A** runs migration and commits the history file: + + ```bash + git add .cds-migration-history.json + git commit -m "Run v8-to-v9 Button migration" + ``` + +2. **Developer B** pulls the changes: + + ```bash + git pull + ``` + +3. **Developer B** tries to run the same migration: + - Migrator detects it was already run + - Shows warning with history + - Recommends skipping + +### Scenario: Different Directories + +If your codebase has multiple directories with different migration states: + +``` +project/ +β”œβ”€β”€ src/ +β”‚ └── .cds-migration-history.json # Migrations complete +└── legacy/ + └── .cds-migration-history.json # Partial migrations +``` + +Each directory tracks its own history independently. + +## Troubleshooting + +### "I need to re-run a transform" + +If a transform failed or needs to be re-run: + +1. **Option 1:** Choose "Re-run all transforms" when prompted by the migrator +2. **Option 2:** Clear history and start fresh: + ```bash + npx @coinbase/cds-migrator --clear-history -p ./src + ``` +3. **Option 3:** Manually edit `.cds-migration-history.json` and remove specific entries + +### "History file is out of sync" + +If team members have conflicting history files: + +1. Review both history files +2. Merge the entries manually in JSON +3. Keep the most complete history +4. Resolve conflicts through code review + +### "Transform was run but not recorded" + +This can happen if: + +- The migration was run in dry-run mode (by design) +- The migrator crashed before saving history +- File permissions prevented writing the history file + +Solution: Manually add the entry to `.cds-migration-history.json` + +## Clearing Migration History + +### Quick Command + +```bash +# Clear history with confirmation +npx @coinbase/cds-migrator --clear-history -p ./src + +# Skip confirmation (be careful!) +npx @coinbase/cds-migrator --clear-history -p ./src --skip-confirmation +``` + +### When to Clear History + +#### Failed Migration + +If a migration failed partway through: + +```bash +# Migration failed +npx @coinbase/cds-migrator -p v8-to-v9 -p ./src --all +# Error occurred, some transforms completed but others didn't + +# Clear history to start fresh +npx @coinbase/cds-migrator --clear-history -p ./src + +# Run migration again +npx @coinbase/cds-migrator -p v8-to-v9 -p ./src --all +``` + +#### Testing Migrations + +During development or testing: + +```bash +# Test a migration +npx @coinbase/cds-migrator -p v8-to-v9 -p ./test-dir --all + +# Clear to test again +npx @coinbase/cds-migrator --clear-history -p ./test-dir --skip-confirmation + +# Re-run with changes +npx @coinbase/cds-migrator -p v8-to-v9 -p ./test-dir --all +``` + +#### Corrupted History File + +If the history file is corrupted or invalid: + +```bash +# Clear the corrupted history +npx @coinbase/cds-migrator --clear-history -p ./src --skip-confirmation + +# Start fresh +npx @coinbase/cds-migrator -p v8-to-v9 -p ./src --all +``` + +#### Reset After Rollback + +If you rolled back code changes via git: + +```bash +# Rolled back code +git reset --hard HEAD~1 + +# Clear history to match code state +npx @coinbase/cds-migrator --clear-history -p ./src --skip-confirmation +``` + +### What Gets Deleted + +The command removes: + +- `.cds-migration-history.json` in the specified path + +It does NOT delete: + +- Your code changes +- The `migration.log` file +- Any other files + +### Bulk Clear + +To clear history for multiple directories: + +```bash +#!/bin/bash +# clear-all-history.sh + +DIRS=("./src" "./lib" "./packages") + +for dir in "${DIRS[@]}"; do + echo "Clearing history for: $dir" + npx @coinbase/cds-migrator --clear-history -p "$dir" --skip-confirmation +done + +echo "βœ… All history cleared" +``` + +### Alternative: Manual Deletion + +You can also delete the file directly: + +```bash +# Delete history file +rm ./src/.cds-migration-history.json + +# Verify it's gone +ls -la ./src/.cds-migration-history.json +``` + +This is equivalent to `--clear-history` but skips all checks. + +### Best Practices + +**βœ… Do:** + +- Clear history after failed migrations +- Clear during testing/development +- Use `--skip-confirmation` in scripts + +**❌ Don't:** + +- Clear history just to re-run transforms (use "Re-run" option instead) +- Clear history without good reason in production +- Clear history if team members are mid-migration + +## Configuration + +### Disable History Tracking + +Currently, history tracking is always enabled for non-dry-run migrations. If you need to disable it: + +1. Use dry-run mode (doesn't record history) +2. Or delete the history file after each run (not recommended) + +### Custom History Location + +The history file is always created in the target directory. This ensures: + +- Each directory can be migrated independently +- History travels with the code +- No global state to manage diff --git a/packages/migrator/docs/QUICK_START.md b/packages/migrator/docs/QUICK_START.md new file mode 100644 index 000000000..65a1288d1 --- /dev/null +++ b/packages/migrator/docs/QUICK_START.md @@ -0,0 +1,371 @@ +# Quick Start Guide + +Get started with CDS Migrator in 5 minutes. + +## Installation + +```bash +# Option 1: Run directly with npx (recommended) +npx @coinbase/cds-migrator + +# Option 2: Install as dev dependency +yarn add -D @coinbase/cds-migrator +``` + +## Your First Migration + +### 1. Run the CLI + +```bash +cd your-project +npx @coinbase/cds-migrator +``` + +### 2. Follow the Prompts + +**Choose migration version:** + +``` +? Which migration preset do you need? + ❯ v8 to v9 - Migrate from CDS v8 to v9 +``` + +**Choose scope:** + +``` +? What would you like to migrate? + ❯ Everything (all changes) + By category (components, hooks, etc.) + By item (specific component/hook/utility) + By specific transform +``` + +**Choose path and mode:** + +``` +? Enter the path to your codebase: ./src +? Run in dry-run mode? (Y/n) Y +``` + +### 3. Review Changes + +The migrator will: + +- Show a migration plan +- Run transforms and display progress +- Create a `migration.log` file with details +- Add TODO comments where manual work is needed + +### 4. Apply Changes + +Once you're satisfied with the dry-run: + +1. Run the migrator again +2. Choose **No** for dry-run mode +3. Review and commit changes + +## Migration Modes + +### Dry-Run Mode (Recommended First) + +Preview changes without modifying files: + +- βœ… Safe to run anytime +- βœ… See what would change +- βœ… Review migration.log +- βœ… No history recorded + +```bash +? Run in dry-run mode? Yes +``` + +### Apply Mode + +Apply changes to your files: + +- ⚠️ Modifies your files +- ⚠️ Records migration history +- ⚠️ Can't easily undo (use git!) +- βœ… Prevents duplicate runs + +```bash +? Run in dry-run mode? No +⚠️ This will modify your files. Continue? (y/N) +``` + +## What Gets Created + +After running a migration: + +``` +your-project/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ .cds-migration-history.json # ← Tracks applied migrations +β”‚ └── ... (your modified files) +└── migration.log # ← Detailed log of changes +``` + +**Important:** + +- βœ… Commit `.cds-migration-history.json` to git +- ❌ Don't commit `migration.log` (review then delete) + +## Selection Strategies + +The migrator offers **four ways to select** what to migrate, with each level offering an "All" option. + +### 1. Everything - Fastest Path + +**Best for:** Initial migration, small codebase, want it done quickly + +``` +? What would you like to migrate? + ❯ Everything (all changes) +``` + +**What happens:** Runs all transforms immediately - no further selections needed. + +### 2. By Category + +**Best for:** Focus on specific types of changes, or see categories before migrating all + +``` +? What would you like to migrate? + ❯ By category (components, hooks, etc.) + +? Select categories to migrate: + β—― πŸ”˜ All categories ← Migrates everything + β—― components - Component API changes + β—― hooks - Hook API changes + β—― utilities - Utility function changes +``` + +Select "πŸ”˜ All categories" to migrate everything, or pick specific ones for phased migration. + +### 3. By Item + +**Best for:** Migrate specific components/hooks, or see full list before migrating all + +``` +? What would you like to migrate? + ❯ By item (specific component/hook/utility) + +? Select items to migrate: + β—― πŸ”˜ All items ← Migrates everything + β—― components.Button - Button component changes (@coinbase/cds-web) + β—― components.Input - Input component changes (@coinbase/cds-web) + β—― hooks.useTheme - useTheme hook changes (@coinbase/cds-common) + β—― utilities.formatCurrency - formatCurrency changes (@coinbase/cds-utils) +``` + +Select "πŸ”˜ All items" to migrate everything, or pick specific items to test one at a time. + +### 4. By Transform + +**Best for:** Maximum control, or review each change before migrating all + +``` +? What would you like to migrate? + ❯ By specific transform + +? Select transforms to run: + β—― πŸ”˜ All transforms ← Migrates everything + β—― components.Button.button-variant-to-appearance - Rename 'variant' prop + β—― components.Input.input-size-values - Update size values + β—― hooks.useTheme.use-theme-return-type - Update return type + β—― utilities.formatCurrency.format-currency-options - Update options +``` + +Select "πŸ”˜ All transforms" to migrate everything after reviewing details, or pick specific ones for surgical changes. + +## Understanding the Output + +### Console Output + +``` +πŸ”„ Running v8-to-v9 migration... + + β†’ Running transform: components.Button.button-variant-to-appearance + Rename 'variant' prop to 'appearance' + + Command: npx jscodeshift --dry --parser=tsx ... + + Processing 25 files... + Transforming src/Button.tsx... + Transforming src/App.tsx... + + βœ“ Transform completed: button-variant-to-appearance + +βœ… Migration completed successfully! +πŸ“ Migration log written to: migration.log +``` + +### Migration Log (`migration.log`) + +``` +CDS Migration Log +Generated: 2026-02-11T12:00:00.000Z +======================================== + +[2026-02-11T12:00:01.000Z] INFO: Starting v8-to-v9 migration +[2026-02-11T12:00:02.000Z] SUCCESS [src/Button.tsx:15]: Renamed prop +[2026-02-11T12:00:03.000Z] TODO [src/Complex.tsx:45]: Manual migration required + This component needs manual review + +======================================== +Migration Summary +======================================== + +Total Entries: 3 +- INFO: 1 +- SUCCESS: 1 +- TODO: 1 + +Manual Migration Required (1 items): + - src/Complex.tsx: Manual migration required +``` + +### TODO Comments in Code + +When automatic migration isn't possible: + +```tsx +// Before migration +; +} +``` + +### Run in Dry-Run + +```bash +cd packages/migrator +node esm/cli.js +# Select: By specific transform +# Select your transform +# Path: /tmp/migration-test/src +# Dry-run: Yes +``` + +### Verify Output + +Check: + +- βœ… Correct files were modified +- βœ… Changes are accurate +- βœ… TODOs added where needed +- βœ… No unintended side effects + +### Apply and Test + +```bash +# Run without dry-run +node esm/cli.js +# Dry-run: No + +# Test the migrated code +cd /tmp/migration-test +yarn install +yarn build +yarn test +``` + +## Resources + +- [jscodeshift Documentation](https://github.com/facebook/jscodeshift) +- [AST Explorer](https://astexplorer.net/) - Visualize JavaScript/TypeScript AST +- [Utilities API](./UTILITIES.md) - Available helper functions +- [Configuration Guide](./CONFIGURATION.md) - Config structure +- [CLI Reference](./CLI_REFERENCE.md) - Running the migrator + +## Common Issues + +### "Transform not found" + +Error: `ENOENT: no such file or directory` + +**Solution:** Check the `file` path in config.json is correct relative to the version directory. + +### "No changes detected" + +The transform runs but no files are modified. + +**Possible causes:** + +- Transform logic doesn't match the code pattern +- Files don't contain the target pattern +- Transform is checking the wrong node type + +**Debug with AST Explorer:** Paste your code and see the actual AST structure. + +### "Transform runs on wrong files" + +**Solution:** Specify `extensions` in config.json: + +```json +{ + "name": "mobile-specific", + "file": "transforms/mobile-specific.ts", + "extensions": "tsx,jsx" // Only JSX files +} +``` diff --git a/packages/migrator/jest.config.js b/packages/migrator/jest.config.js new file mode 100644 index 000000000..733749904 --- /dev/null +++ b/packages/migrator/jest.config.js @@ -0,0 +1,14 @@ +const config = { + preset: '../../jest.preset.js', + displayName: 'cds-migrator', + // Migrator tests execute codemods and filesystem helpers, so Node is the correct runtime. + testEnvironment: 'node', + coverageReporters: ['text-summary', 'text', 'json-summary'], + moduleNameMapper: { + // Transform source imports use ESM ".js" specifiers, but Jest runs TS sources in tests. + // Remap relative ".js" imports to extensionless paths so Jest resolves TS modules correctly. + '^(\\.{1,2}/.*)\\.js$': '$1', + }, +}; + +export default config; diff --git a/packages/migrator/package.json b/packages/migrator/package.json new file mode 100644 index 000000000..424b39848 --- /dev/null +++ b/packages/migrator/package.json @@ -0,0 +1,49 @@ +{ + "name": "@coinbase/cds-migrator", + "version": "1.0.0", + "description": "Coinbase Design System - Code Migration Tools", + "repository": { + "type": "git", + "url": "git@github.com:coinbase/cds.git", + "directory": "packages/migrator" + }, + "type": "module", + "main": "./esm/index.js", + "types": "./dts/index.d.ts", + "bin": { + "cds-migrate": "./esm/cli.js", + "cds-migrate:all": "./esm/bin/migrate-all.js", + "cds-migrate:clear-history": "./esm/bin/clear-history.js" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dts/index.d.ts", + "default": "./esm/index.js" + }, + "./*": { + "types": "./dts/*.d.ts", + "default": "./esm/*.js" + } + }, + "sideEffects": false, + "files": [ + "dts", + "esm", + "CHANGELOG" + ], + "dependencies": { + "commander": "^11.1.0", + "inquirer": "^9.2.12", + "jscodeshift": "^0.15.1" + }, + "devDependencies": { + "@babel/core": "^7.28.0", + "@babel/preset-env": "^7.28.0", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.27.1", + "@types/inquirer": "^9.0.7", + "@types/jscodeshift": "^0.11.10", + "@types/node": "^20.0.0" + } +} diff --git a/packages/migrator/project.json b/packages/migrator/project.json new file mode 100644 index 000000000..5025cb97b --- /dev/null +++ b/packages/migrator/project.json @@ -0,0 +1,45 @@ +{ + "name": "migrator", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/migrator/src", + "projectType": "library", + "targets": { + "build": { + "executor": "nx:run-commands", + "defaultConfiguration": "dev", + "configurations": { + "dev": { + "command": "rm -rf esm && babel ./src --out-dir esm --extensions .ts,.tsx,.js,.jsx --copy-files --no-copy-ignored" + }, + "prod": { + "commands": [ + "rm -rf esm && babel ./src --out-dir esm --extensions .ts,.tsx,.js,.jsx --copy-files --no-copy-ignored" + ], + "parallel": false + } + } + }, + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "nx:run-commands", + "options": { + "command": "jest --maxWorkers=75%", + "cwd": "{projectRoot}" + } + }, + "typecheck": { + "executor": "nx:run-commands", + "defaultConfiguration": "dev", + "configurations": { + "dev": { + "command": "tsc --build --pretty --verbose" + }, + "prod": { + "command": "tsc --build ./tsconfig.build.json --pretty --verbose" + } + } + } + } +} diff --git a/packages/migrator/src/bin/clear-history.ts b/packages/migrator/src/bin/clear-history.ts new file mode 100644 index 000000000..d73dffebd --- /dev/null +++ b/packages/migrator/src/bin/clear-history.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +/** + * cds-migrate:clear-history + * Quick command to clear migration history + */ + +import { Command } from 'commander'; +import inquirer from 'inquirer'; + +import { clearMigrationHistory, loadMigrationHistory } from '../utils/index.js'; + +const program = new Command(); + +program + .name('cds-migrate:clear-history') + .description('Clear CDS migration history') + .option('--path ', 'Target path', './src') + .option('--skip-confirmation', 'Skip confirmation prompt', false) + .parse(); + +const options = program.opts(); + +async function main() { + const history = loadMigrationHistory(options.path); + + if (!history) { + console.log(`No migration history found at: ${options.path}`); + return; + } + + if (!options.skipConfirmation) { + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: `⚠️ Clear migration history for ${options.path}?`, + default: false, + }, + ]); + + if (!confirm) { + console.log('❌ Cancelled.\n'); + return; + } + } + + clearMigrationHistory(options.path); + console.log(`βœ… Migration history cleared for: ${options.path}\n`); +} + +main().catch((error) => { + console.error('Error:', error); + process.exit(1); +}); diff --git a/packages/migrator/src/bin/migrate-all.ts b/packages/migrator/src/bin/migrate-all.ts new file mode 100644 index 000000000..9bb330589 --- /dev/null +++ b/packages/migrator/src/bin/migrate-all.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +/** + * cds-migrate:all + * Quick command to migrate everything with a preset + */ + +import { Command } from 'commander'; + +import { runMigration } from '../runner.js'; + +const program = new Command(); + +program + .name('cds-migrate:all') + .description('Migrate everything with CDS migration preset') + .requiredOption('-p, --preset ', 'Migration preset (e.g., v8-to-v9)') + .option('--path ', 'Target path to migrate', './src') + .option('-d, --dry-run', 'Run in dry-run mode', false) + .parse(); + +const options = program.opts(); + +async function main() { + console.log('\nπŸš€ CDS Migrator - Migrate All\n'); + console.log(`Preset: ${options.preset}`); + console.log(`Path: ${options.path}`); + console.log(`Mode: ${options.dryRun ? 'Dry Run' : 'Apply Changes'}\n`); + + await runMigration({ + preset: options.preset, + path: options.path, + dryRun: options.dryRun, + selection: { all: true }, + }); + + console.log('\nβœ… Migration completed!\n'); +} + +main().catch((error) => { + console.error('\n❌ Migration failed:', error); + process.exit(1); +}); diff --git a/packages/migrator/src/cli-args.ts b/packages/migrator/src/cli-args.ts new file mode 100644 index 000000000..5ac3e3dc8 --- /dev/null +++ b/packages/migrator/src/cli-args.ts @@ -0,0 +1,79 @@ +/** + * CLI argument parsing + */ + +import { Command } from 'commander'; + +import type { MigrationSelection } from './types.js'; + +export type CliArgs = { + preset?: string; + path?: string; + dryRun?: boolean; + skipConfirmation?: boolean; + clearHistory?: boolean; + all?: boolean; + category?: string[]; + item?: string[]; + transform?: string[]; +}; + +export function parseCliArgs(): CliArgs { + const program = new Command(); + + program + .name('cds-migrate') + .description('CDS code migration tool') + .version('1.0.0') + .option('-p, --preset ', 'Migration preset (e.g., v8-to-v9)') + .option('--path ', 'Target path to migrate', './src') + .option('-d, --dry-run', 'Run in dry-run mode (preview changes)', false) + .option('--skip-confirmation', 'Skip confirmation prompts', false) + .option('--clear-history', 'Clear migration history for the specified path', false) + .option('--all', 'Migrate everything') + .option('--category ', 'Migrate specific categories') + .option('--item ', 'Migrate specific items (components/hooks/utilities)') + .option('--transform ', 'Migrate specific transforms'); + + program.parse(); + + const options = program.opts(); + + return { + preset: options.preset, + path: options.path, + dryRun: options.dryRun, + skipConfirmation: options.skipConfirmation, + clearHistory: options.clearHistory, + all: options.all, + category: options.category, + item: options.item, + transform: options.transform, + }; +} + +export function buildSelectionFromArgs(args: CliArgs): MigrationSelection | null { + if (args.all) { + return { all: true }; + } + + if (args.transform && args.transform.length > 0) { + return { transforms: args.transform }; + } + + if (args.item && args.item.length > 0) { + return { items: args.item }; + } + + if (args.category && args.category.length > 0) { + return { categories: args.category }; + } + + return null; +} + +export function hasRequiredArgs(args: CliArgs): boolean { + // Preset and path are required for non-interactive mode + // Plus at least one selection method + return !!(args.preset && args.path && (args.all || args.category || args.item || args.transform)); +} diff --git a/packages/migrator/src/cli.ts b/packages/migrator/src/cli.ts new file mode 100644 index 000000000..3c920c1c0 --- /dev/null +++ b/packages/migrator/src/cli.ts @@ -0,0 +1,380 @@ +#!/usr/bin/env node + +/** + * CDS Migrator CLI + * + * Interactive tool to migrate codebases between CDS major versions + * Also supports non-interactive mode via CLI flags + */ + +import inquirer from 'inquirer'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { + buildMigrationSummary, + getSelectedTransforms, + loadMigrationConfig, +} from './utils/config-loader.js'; +import { + buildHistorySummary, + clearMigrationHistory, + getAlreadyRunTransforms, + loadMigrationHistory, +} from './utils/migration-history.js'; +import { buildSelectionFromArgs, type CliArgs, hasRequiredArgs, parseCliArgs } from './cli-args.js'; +import { runMigration } from './runner.js'; +import type { MigrationSelection } from './types.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const AVAILABLE_PRESETS = [ + { + name: 'v8 to v9', + value: 'v8-to-v9', + description: 'Migrate from CDS v8 to v9', + }, +] as const; + +async function selectMigrationScope(presetDir: string): Promise { + const config = loadMigrationConfig(presetDir); + + // Step 1: Select category + const categoryChoices = [ + { + name: 'πŸ”˜ All categories', + value: '__ALL__', + }, + ...Object.entries(config.categories).map(([name, cat]) => ({ + name: `${name} - ${cat.description}`, + value: name, + })), + ]; + + const { selectedCategory } = await inquirer.prompt([ + { + type: 'list', + name: 'selectedCategory', + message: 'Which category of transforms do you want to run?', + choices: categoryChoices, + }, + ]); + + // If "All categories" is selected, migrate everything + if (selectedCategory === '__ALL__') { + return { all: true }; + } + + // Step 2: Select transform in the selected category + const category = config.categories[selectedCategory]; + if (!category) { + return { all: true }; + } + + // Build list of all transforms in this category + const transformChoices = [ + { + name: `πŸ”˜ All transforms in ${selectedCategory}`, + value: '__ALL__', + }, + ]; + + for (const [itemName, item] of Object.entries(category.variables)) { + for (const transform of item.transforms) { + transformChoices.push({ + name: `${itemName}.${transform.name} - ${transform.description}`, + value: `${selectedCategory}.${itemName}.${transform.name}`, + }); + } + } + + const { selectedTransform } = await inquirer.prompt([ + { + type: 'list', + name: 'selectedTransform', + message: `Which transform in "${selectedCategory}" category?`, + choices: transformChoices, + }, + ]); + + // If "All transforms" selected, run all in this category + if (selectedTransform === '__ALL__') { + return { categories: [selectedCategory] }; + } + + // Return the specific transform + return { transforms: [selectedTransform] }; +} + +async function runInteractiveMode() { + // Step 1: Select migration preset + const answers = await inquirer.prompt([ + { + type: 'list', + name: 'preset', + message: 'Which migration preset do you need?', + choices: AVAILABLE_PRESETS.map((m) => ({ + name: `${m.name} - ${m.description}`, + value: m.value, + })), + }, + ]); + + // Step 2: Ask for target path + const pathAnswer = await inquirer.prompt([ + { + type: 'input', + name: 'path', + message: 'Enter the path to your codebase (relative or absolute):', + default: './src', + validate: (input: string) => { + if (!input || input.trim() === '') { + return 'Please provide a valid path'; + } + return true; + }, + }, + ]); + + const targetPath = pathAnswer.path; + + // Step 3: Check and display migration history + const history = loadMigrationHistory(targetPath); + if (history && history.entries.length > 0) { + const historySummary = buildHistorySummary(targetPath); + console.log(historySummary); + } + + // Step 4: Load config and let user select scope + const presetDir = path.join(__dirname, answers.preset); + const selection = await selectMigrationScope(presetDir); + + // Step 5: Show what will be migrated + const config = loadMigrationConfig(presetDir); + const summary = buildMigrationSummary(config, selection); + console.log(summary); + + // Step 6: Ask for dry-run mode + const modeAnswer = await inquirer.prompt([ + { + type: 'confirm', + name: 'dryRun', + message: 'Run in dry-run mode? (preview changes without modifying files)', + default: true, + }, + ]); + + // Check for migration history and warn about duplicate runs (skip if dry-run) + const selectedTransforms = getSelectedTransforms(config, selection); + const transformIds = selectedTransforms.map((t) => `${t.category}.${t.variable}.${t.name}`); + const alreadyRun = getAlreadyRunTransforms(targetPath, transformIds); + + // Only warn about duplicates if NOT in dry-run mode + if (alreadyRun.length > 0 && !modeAnswer.dryRun) { + console.log('\n⚠️ Warning: Some transforms have already been run on this path:\n'); + for (const transformId of alreadyRun) { + console.log(` β€’ ${transformId}`); + } + console.log(''); + + const { action } = await inquirer.prompt([ + { + type: 'list', + name: 'action', + message: 'How would you like to proceed?', + choices: [ + { + name: 'Skip already-run transforms (recommended)', + value: 'skip', + }, + { + name: 'Re-run all transforms (may cause issues)', + value: 'rerun', + }, + { + name: 'Cancel migration', + value: 'cancel', + }, + ], + default: 'skip', + }, + ]); + + if (action === 'cancel') { + console.log('\n❌ Migration cancelled.\n'); + return; + } + + if (action === 'skip') { + // Filter out already-run transforms + if (selection.transforms) { + selection.transforms = selection.transforms.filter((t) => !alreadyRun.includes(t)); + } else if (selection.items) { + // For item selection, we need to filter at the item level + // This is complex, so we'll just warn the user + console.log('\n⚠️ Note: Skipping at the item level may leave some transforms incomplete.'); + console.log('Consider using transform-level selection for more control.\n'); + } + // For category or all selection, we can't easily filter, so just warn + if (selection.all || selection.categories) { + console.log( + '\n⚠️ Note: Cannot skip at category level. Some transforms may be skipped during execution.\n', + ); + } + } + } + + console.log('\nπŸ“‹ Migration Summary:'); + console.log(` Preset: ${answers.preset}`); + console.log(` Path: ${targetPath}`); + console.log(` Mode: ${modeAnswer.dryRun ? 'Dry Run' : 'Apply Changes'}\n`); + + if (!modeAnswer.dryRun) { + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: '⚠️ This will modify your files. Continue?', + default: false, + }, + ]); + + if (!confirm) { + console.log('\n❌ Migration cancelled.\n'); + return; + } + } + + return { + preset: answers.preset, + path: targetPath, + dryRun: modeAnswer.dryRun, + selection, + }; +} + +async function runNonInteractiveMode(args: CliArgs) { + const preset = args.preset!; + const targetPath = args.path!; + const dryRun = args.dryRun || false; + const selection = buildSelectionFromArgs(args); + + if (!selection) { + console.error('Error: Must specify --all, --category, --item, or --transform'); + process.exit(1); + } + + // Validate preset + const isValidPreset = AVAILABLE_PRESETS.some((p) => p.value === preset); + if (!isValidPreset) { + console.error(`Error: Invalid preset '${preset}'`); + console.error(`Available presets: ${AVAILABLE_PRESETS.map((p) => p.value).join(', ')}`); + process.exit(1); + } + + // Load config + const presetDir = path.join(__dirname, preset); + const config = loadMigrationConfig(presetDir); + + // Show migration plan + const summary = buildMigrationSummary(config, selection); + console.log(summary); + + // Check for duplicates + const selectedTransforms = getSelectedTransforms(config, selection); + const transformIds = selectedTransforms.map((t) => `${t.category}.${t.variable}.${t.name}`); + const alreadyRun = getAlreadyRunTransforms(targetPath, transformIds); + + if (alreadyRun.length > 0 && !dryRun && !args.skipConfirmation) { + console.log('\n⚠️ Warning: Some transforms have already been run on this path:\n'); + for (const transformId of alreadyRun) { + console.log(` β€’ ${transformId}`); + } + console.log('\nUse --skip-confirmation to bypass this check or run with --dry-run first.\n'); + process.exit(1); + } + + console.log('\nπŸ“‹ Migration Summary:'); + console.log(` Preset: ${preset}`); + console.log(` Path: ${targetPath}`); + console.log(` Mode: ${dryRun ? 'Dry Run' : 'Apply Changes'}`); + console.log(` Transforms: ${selectedTransforms.length}\n`); + + return { + preset: preset, + path: targetPath, + dryRun, + selection, + }; +} + +async function main() { + console.log('\nπŸš€ CDS Migrator\n'); + + // Parse CLI arguments + const args = parseCliArgs(); + + // Handle clear-history command + if (args.clearHistory) { + if (!args.path) { + console.error('Error: --path is required when using --clear-history'); + process.exit(1); + } + + const history = loadMigrationHistory(args.path); + if (!history) { + console.log(`No migration history found at: ${args.path}`); + return; + } + + const { confirm } = await inquirer.prompt([ + { + type: 'confirm', + name: 'confirm', + message: `⚠️ Clear migration history for ${args.path}?`, + default: false, + }, + ]); + + if (confirm) { + clearMigrationHistory(args.path); + console.log(`βœ… Migration history cleared for: ${args.path}\n`); + } else { + console.log('❌ Cancelled.\n'); + } + return; + } + + // Determine if running in interactive or non-interactive mode + const isNonInteractive = hasRequiredArgs(args); + + let migrationOptions; + + if (isNonInteractive) { + // Non-interactive mode with CLI flags + migrationOptions = await runNonInteractiveMode(args); + } else { + // Interactive mode with prompts + console.log('This tool will help you migrate your codebase between CDS major versions.\n'); + migrationOptions = await runInteractiveMode(); + } + + if (!migrationOptions) { + console.error('\n❌ Migration setup failed.\n'); + process.exit(1); + } + + try { + await runMigration(migrationOptions); + console.log('\nβœ… Migration completed successfully!\n'); + } catch (error) { + console.error('\n❌ Migration failed:', error); + process.exit(1); + } +} + +main().catch((error) => { + console.error('Unexpected error:', error); + process.exit(1); +}); diff --git a/packages/migrator/src/index.ts b/packages/migrator/src/index.ts new file mode 100644 index 000000000..45ec62290 --- /dev/null +++ b/packages/migrator/src/index.ts @@ -0,0 +1,9 @@ +/** + * @coinbase/cds-migrator + * + * Code migration tools for the Coinbase Design System + */ + +export { runMigration } from './runner.js'; +export * from './types.js'; +export * from './utils/index.js'; diff --git a/packages/migrator/src/runner.ts b/packages/migrator/src/runner.ts new file mode 100644 index 000000000..b0994bbf6 --- /dev/null +++ b/packages/migrator/src/runner.ts @@ -0,0 +1,127 @@ +/** + * Migration runner + * + * Coordinates the execution of version-specific migrations + */ + +import { execSync } from 'child_process'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { + createLogger, + getSelectedTransforms, + hasTransformBeenRun, + loadMigrationConfig, + recordTransformRun, +} from './utils/index.js'; +import type { MigrationSelection } from './types.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +type RunMigrationOptions = { + preset: string; + path: string; + dryRun: boolean; + selection?: MigrationSelection; +}; + +export async function runMigration(options: RunMigrationOptions): Promise { + const { preset, path: targetPath, dryRun, selection = { all: true } } = options; + + console.log(`\nπŸ”„ Running ${preset} migration...\n`); + + // Create logger for this migration run + const logger = createLogger(process.cwd()); + logger.info(`Starting ${preset} migration`); + logger.info(`Target path: ${targetPath}`); + logger.info(`Mode: ${dryRun ? 'Dry Run' : 'Apply Changes'}`); + + // Load config and get selected transforms + const presetDir = path.join(__dirname, preset); + const config = loadMigrationConfig(presetDir); + const transforms = getSelectedTransforms(config, selection); + + if (transforms.length === 0) { + console.log('ℹ️ No transforms selected for this migration.'); + logger.warn('No transforms selected'); + return; + } + + logger.info(`Selected ${transforms.length} transforms to run`); + + // Run each transform using jscodeshift + let skippedCount = 0; + + for (const transform of transforms) { + const transformId = `${transform.category}.${transform.variable}.${transform.name}`; + + // Check if this transform has already been run + if (!dryRun && hasTransformBeenRun(targetPath, transformId)) { + console.log(`\n ⊘ Skipping (already run): ${transformId}`); + logger.info(`Skipped transform (already run): ${transformId}`); + skippedCount++; + continue; + } + + console.log(`\n β†’ Running transform: ${transformId}`); + console.log(` ${transform.description}`); + logger.info( + `Running transform: ${transform.name} (${transform.category}.${transform.variable})`, + ); + + // Add .js extension if not present (transforms are compiled from .ts to .js) + const transformFile = transform.file.endsWith('.js') ? transform.file : `${transform.file}.js`; + const transformPath = path.join(presetDir, transformFile); + const extensions = transform.extensions || 'tsx,ts,jsx,js'; + + try { + const jscodeshiftCmd = [ + 'npx', + 'jscodeshift', + dryRun ? '--dry' : '', + '--parser=tsx', + `--extensions=${extensions}`, + '--ignore-pattern="**/node_modules/**"', + '--ignore-pattern="**/.next/**"', + '--ignore-pattern="**/dist/**"', + '--ignore-pattern="**/build/**"', + `--transform=${transformPath}`, + targetPath, + ] + .filter(Boolean) + .join(' '); + + console.log(` Command: ${jscodeshiftCmd}\n`); + + execSync(jscodeshiftCmd, { + stdio: 'inherit', + cwd: process.cwd(), + }); + + console.log(`\n βœ“ Transform completed: ${transform.name}`); + logger.success(`Transform completed: ${transform.name}`); + + // Record this transform run in history (only for non-dry-runs) + recordTransformRun(targetPath, transformId, preset, dryRun); + } catch (error) { + console.error(`\n βœ— Transform failed: ${transform.name}`); + logger.error( + `Transform failed: ${transform.name}`, + undefined, + undefined, + error instanceof Error ? error.message : String(error), + ); + throw error; + } + } + + if (skippedCount > 0) { + console.log(`\n⊘ Skipped ${skippedCount} transform(s) that were already run.\n`); + logger.info(`Skipped ${skippedCount} transforms that were already run`); + } + + // Write summary and close logger + logger.writeSummary(); +} diff --git a/packages/migrator/src/types.ts b/packages/migrator/src/types.ts new file mode 100644 index 000000000..85172cea8 --- /dev/null +++ b/packages/migrator/src/types.ts @@ -0,0 +1,118 @@ +/** + * Types for CDS migration tools + */ + +export type MigrationOptions = { + /** + * Paths to files or directories to migrate + */ + paths: string[]; + /** + * Whether to perform a dry run without modifying files + */ + dryRun?: boolean; +}; + +export type Transform = { + /** + * Name of the transform + */ + name: string; + /** + * Description of what the transform does + */ + description: string; + /** + * Path to the transform file (relative to migration directory) + */ + file: string; + /** + * File extensions to process (comma-separated) + * @default "tsx,ts,jsx,js" + */ + extensions?: string; +}; + +export type MigrationModule = { + /** + * List of transforms to run for this migration + */ + transforms: Transform[]; + /** + * Description of the migration and breaking changes + */ + description: string; +}; + +/** + * Configuration for a migration variable (component, hook, utility, etc.) + */ +export type MigrationVariable = { + /** + * Human-readable description of the changes + */ + description: string; + /** + * Package name where this variable is exported from + */ + package: string; + /** + * List of transforms to apply for this variable + */ + transforms: Transform[]; +}; + +/** + * Configuration for a migration category + */ +export type MigrationCategory = { + /** + * Human-readable description of the category + */ + description: string; + /** + * Variables (components, hooks, utilities, etc.) in this category + */ + variables: Record; +}; + +/** + * Main migration configuration structure + */ +export type MigrationConfig = { + /** + * Preset identifier (e.g., "v8-to-v9") + */ + preset: string; + /** + * Overall description of the migration + */ + description: string; + /** + * Categories of changes organized by type + */ + categories: Record; +}; + +/** + * Selection for what to migrate + */ +export type MigrationSelection = { + /** + * If true, migrate everything + */ + all?: boolean; + /** + * Specific categories to migrate + */ + categories?: string[]; + /** + * Specific items to migrate (format: "category.item") + * Items are components, hooks, utilities, etc. + */ + items?: string[]; + /** + * Specific transforms to migrate (format: "category.item.transform") + */ + transforms?: string[]; +}; diff --git a/packages/migrator/src/types/jscodeshift.d.ts b/packages/migrator/src/types/jscodeshift.d.ts new file mode 100644 index 000000000..20a339dfc --- /dev/null +++ b/packages/migrator/src/types/jscodeshift.d.ts @@ -0,0 +1,11 @@ +declare module 'jscodeshift' { + export function withParser(parser: string): any; + + export type API = any; + export type ASTPath = any; + export type Collection = any; + export type FileInfo = any; + export type JSXAttribute = any; + export type JSXOpeningElement = any; + export type Options = any; +} diff --git a/packages/migrator/src/utils/config-loader.ts b/packages/migrator/src/utils/config-loader.ts new file mode 100644 index 000000000..6a48694d2 --- /dev/null +++ b/packages/migrator/src/utils/config-loader.ts @@ -0,0 +1,156 @@ +/** + * Configuration loader utilities + */ + +import fs from 'fs'; +import path from 'path'; + +import type { MigrationConfig, MigrationSelection, Transform } from '../types.js'; + +/** + * Load migration configuration from config.json + */ +export function loadMigrationConfig(migrationDir: string): MigrationConfig { + const configPath = path.join(migrationDir, 'config.json'); + + if (!fs.existsSync(configPath)) { + throw new Error(`Migration config not found: ${configPath}`); + } + + const configContent = fs.readFileSync(configPath, 'utf-8'); + return JSON.parse(configContent) as MigrationConfig; +} + +/** + * Get all transforms from config based on selection + */ +export function getSelectedTransforms( + config: MigrationConfig, + selection: MigrationSelection, +): Array { + const transforms: Array = []; + + // If migrate all, collect everything + if (selection.all) { + for (const [categoryName, category] of Object.entries(config.categories)) { + for (const [variableName, variable] of Object.entries(category.variables)) { + for (const transform of variable.transforms) { + transforms.push({ + ...transform, + category: categoryName, + variable: variableName, + }); + } + } + } + return transforms; + } + + // Collect by specific transforms + if (selection.transforms && selection.transforms.length > 0) { + for (const transformPath of selection.transforms) { + const [categoryName, variableName, transformName] = transformPath.split('.'); + const transform = config.categories[categoryName]?.variables[variableName]?.transforms.find( + (t) => t.name === transformName, + ); + if (transform) { + transforms.push({ + ...transform, + category: categoryName, + variable: variableName, + }); + } + } + return transforms; + } + + // Collect by specific items + if (selection.items && selection.items.length > 0) { + for (const itemPath of selection.items) { + const [categoryName, itemName] = itemPath.split('.'); + const item = config.categories[categoryName]?.variables[itemName]; + if (item) { + for (const transform of item.transforms) { + transforms.push({ + ...transform, + category: categoryName, + variable: itemName, + }); + } + } + } + return transforms; + } + + // Collect by specific categories + if (selection.categories && selection.categories.length > 0) { + for (const categoryName of selection.categories) { + const category = config.categories[categoryName]; + if (category) { + for (const [variableName, variable] of Object.entries(category.variables)) { + for (const transform of variable.transforms) { + transforms.push({ + ...transform, + category: categoryName, + variable: variableName, + }); + } + } + } + } + return transforms; + } + + return transforms; +} + +/** + * Build a summary of what will be migrated + */ +export function buildMigrationSummary( + config: MigrationConfig, + selection: MigrationSelection, +): string { + const transforms = getSelectedTransforms(config, selection); + const byCategory: Record> = {}; + + for (const transform of transforms) { + if (!byCategory[transform.category]) { + byCategory[transform.category] = []; + } + byCategory[transform.category].push({ + variable: transform.variable, + transform: transform.name, + }); + } + + let summary = '\nMigration Plan:\n'; + summary += '================\n\n'; + + for (const [category, items] of Object.entries(byCategory)) { + const categoryInfo = config.categories[category]; + summary += `πŸ“¦ ${category}: ${categoryInfo.description}\n`; + + const byVariable: Record = {}; + for (const item of items) { + if (!byVariable[item.variable]) { + byVariable[item.variable] = []; + } + byVariable[item.variable].push(item.transform); + } + + for (const [variable, transformNames] of Object.entries(byVariable)) { + const variableInfo = categoryInfo.variables[variable]; + summary += ` └─ ${variable} (${variableInfo.package})\n`; + for (const transformName of transformNames) { + const transform = variableInfo.transforms.find((t) => t.name === transformName); + summary += ` β€’ ${transform?.description || transformName}\n`; + } + } + summary += '\n'; + } + + summary += `Total transforms: ${transforms.length}\n`; + + return summary; +} diff --git a/packages/migrator/src/utils/constants.ts b/packages/migrator/src/utils/constants.ts new file mode 100644 index 000000000..9a3ecd411 --- /dev/null +++ b/packages/migrator/src/utils/constants.ts @@ -0,0 +1,6 @@ +/** + * Constants used across migration utilities + */ + +export const TODO_PREFIX = 'TODO(cds-migration)'; +export const LOG_FILE_NAME = 'migration.log'; diff --git a/packages/migrator/src/utils/index.ts b/packages/migrator/src/utils/index.ts new file mode 100644 index 000000000..3250657b8 --- /dev/null +++ b/packages/migrator/src/utils/index.ts @@ -0,0 +1,9 @@ +/** + * Shared utility functions for CDS migrations + */ + +export * from './config-loader.js'; +export * from './constants.js'; +export * from './insert-todo.js'; +export * from './logger.js'; +export * from './migration-history.js'; diff --git a/packages/migrator/src/utils/insert-todo.ts b/packages/migrator/src/utils/insert-todo.ts new file mode 100644 index 000000000..8ae3635ab --- /dev/null +++ b/packages/migrator/src/utils/insert-todo.ts @@ -0,0 +1,136 @@ +/** + * Utilities for inserting TODO comments when automatic migration is not possible + */ + +import type { API, Collection } from 'jscodeshift'; + +import { TODO_PREFIX } from './constants.js'; + +export type InsertTodoOptions = { + /** + * The message to include in the TODO comment + */ + message: string; + /** + * Additional context or instructions + */ + context?: string; + /** + * Whether to insert before or after the node + */ + position?: 'before' | 'after'; +}; + +/** + * Insert a TODO comment near a JSX element + * + * @example + * // Before + * + + + ); +} diff --git a/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/basic-literal.output.tsx b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/basic-literal.output.tsx new file mode 100644 index 000000000..d5a77e5a7 --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/basic-literal.output.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +// @ts-expect-error -- Fixture import mirrors codemod target imports; fixture files are not validating package export typings. +import { Button, IconButton } from '@coinbase/cds-web'; + +export function Example() { + return ( + <> + + + + ); +} diff --git a/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/dynamic-variant.input.tsx b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/dynamic-variant.input.tsx new file mode 100644 index 000000000..b20450bd2 --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/dynamic-variant.input.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +// @ts-expect-error -- Fixture import mirrors codemod target imports; fixture files are not validating package export typings. +import { Button } from '@coinbase/cds-web'; + +// @ts-expect-error -- Fixture uses unresolved helper to represent a dynamic runtime variant source. +const variant = getVariant(); + +export function Example() { + return ; +} diff --git a/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/dynamic-variant.output.tsx b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/dynamic-variant.output.tsx new file mode 100644 index 000000000..0b0e9b06d --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/dynamic-variant.output.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +// @ts-expect-error -- Fixture import mirrors codemod target imports; fixture files are not validating package export typings. +import { Button } from '@coinbase/cds-web'; + +// @ts-expect-error -- Fixture uses unresolved helper to represent a dynamic runtime variant source. +const variant = getVariant(); + +export function Example() { + return ( + // TODO(cds-migration): Button variant requires manual migration + // Found dynamic or non-literal variant value. Verify mapping to v9 variants and update manually if needed. + + ); +} diff --git a/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/spread-props.input.tsx b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/spread-props.input.tsx new file mode 100644 index 000000000..d463cc5a2 --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/spread-props.input.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +// @ts-expect-error -- Fixture import mirrors codemod target imports; fixture files are not validating package export typings. +import { IconButton } from '@coinbase/cds-web'; + +const iconProps = { + variant: 'foregroundMuted' as const, + compact: true, +}; + +export function Example() { + return ; +} diff --git a/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/spread-props.output.tsx b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/spread-props.output.tsx new file mode 100644 index 000000000..c2b03a7ca --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/__fixtures__/button-variant-mapping/spread-props.output.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +// @ts-expect-error -- Fixture import mirrors codemod target imports; fixture files are not validating package export typings. +import { IconButton } from '@coinbase/cds-web'; + +const iconProps = { + variant: 'secondary' as const, + compact: true, +}; + +export function Example() { + return ( + // TODO(cds-migration): IconButton spread props need manual review + // Found spread props on IconButton. Spread values may still contain legacy variant values. Review spread source and flatten/update variant explicitly. An obvious local object-literal variant was auto-updated, but full spread safety still requires manual verification. + + ); +} diff --git a/packages/migrator/src/v8-to-v9/transforms/__tests__/button-variant-mapping.test.ts b/packages/migrator/src/v8-to-v9/transforms/__tests__/button-variant-mapping.test.ts new file mode 100644 index 000000000..81e75bfab --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/__tests__/button-variant-mapping.test.ts @@ -0,0 +1,141 @@ +import fs from 'fs'; +import path from 'path'; + +import { TODO_PREFIX } from '../../../utils/constants'; +import transformer from '../button-variant-mapping'; +import { runTransform } from '../test-utils'; + +function readFixtureFile(fileName: string): string { + const fixturePath = path.resolve( + process.cwd(), + 'src/v8-to-v9/transforms/__fixtures__/button-variant-mapping', + fileName, + ); + return fs.readFileSync(fixturePath, 'utf8'); +} + +function normalizeCode(code: string): string { + return code.replace(/\s+/g, '').replace(/"/g, "'"); +} + +describe('button-variant-mapping', () => { + it('maps static literal variants for Button and IconButton', () => { + const source = ` + import { Button, IconButton } from '@coinbase/cds-web'; + + export function Example() { + return ( + <> + + + + + + ); + } + `; + + const { output, entries } = runTransform(transformer, source); + + expect(output).toContain('variant="inverse"'); + expect(output).toContain('variant="secondary"'); + expect(entries.some((entry) => entry.level === 'SUCCESS')).toBe(true); + }); + + it('supports imported aliases', () => { + const source = ` + import { Button as CdsButton, IconButton as CdsIconButton } from '@coinbase/cds-web'; + + export const Example = () => ( + <> + Save + + + ); + `; + + const { output } = runTransform(transformer, source); + + expect(output).toContain(''); + expect(output).toContain(''); + }); + + it('adds TODO for dynamic variants and spread props', () => { + const source = ` + import { Button, IconButton } from '@coinbase/cds-web'; + + const variant = shouldInvert() ? 'tertiary' : getVariant(); + const iconProps = { variant: 'foregroundMuted' as const, compact: true }; + + export function Example() { + return ( + <> + + + + ); + } + `; + + const { output, entries } = runTransform(transformer, source); + + expect(output).toContain(TODO_PREFIX); + expect(output).toMatch(/variant:\s*["']secondary["']/); + expect(entries.filter((entry) => entry.level === 'TODO').length).toBeGreaterThanOrEqual(2); + }); + + it('is idempotent for TODO insertion', () => { + const source = ` + import { Button } from '@coinbase/cds-web'; + + export function Example({ variant, props }: { variant: string; props: Record }) { + return ; + } + `; + + const first = runTransform(transformer, source).output; + const second = runTransform(transformer, first).output; + + const firstTodoCount = first.split(TODO_PREFIX).length - 1; + const secondTodoCount = second.split(TODO_PREFIX).length - 1; + expect(secondTodoCount).toBe(firstTodoCount); + }); + + it('does not change non-target components', () => { + const source = ` + import { Button as NotCdsButton } from './local'; + + export const Example = () => Save; + `; + + const { output, entries } = runTransform(transformer, source); + expect(output).toBe(source); + expect(entries.length).toBe(0); + }); + + it('matches basic-literal fixture output', () => { + const input = readFixtureFile('basic-literal.input.tsx'); + const expected = readFixtureFile('basic-literal.output.tsx'); + + const { output } = runTransform(transformer, input); + expect(normalizeCode(output)).toBe(normalizeCode(expected)); + }); + + it('matches dynamic-variant fixture output', () => { + const input = readFixtureFile('dynamic-variant.input.tsx'); + const expected = readFixtureFile('dynamic-variant.output.tsx'); + + const { output, entries } = runTransform(transformer, input); + expect(normalizeCode(output)).toBe(normalizeCode(expected)); + expect(entries.some((entry) => entry.level === 'TODO')).toBe(true); + }); + + it('matches spread-props fixture output', () => { + const input = readFixtureFile('spread-props.input.tsx'); + const expected = readFixtureFile('spread-props.output.tsx'); + + const { output, entries } = runTransform(transformer, input); + expect(normalizeCode(output)).toBe(normalizeCode(expected)); + expect(entries.some((entry) => entry.level === 'TODO')).toBe(true); + }); +}); diff --git a/packages/migrator/src/v8-to-v9/transforms/button-variant-mapping.ts b/packages/migrator/src/v8-to-v9/transforms/button-variant-mapping.ts new file mode 100644 index 000000000..019acf613 --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/button-variant-mapping.ts @@ -0,0 +1,16 @@ +import type { API, FileInfo, Options } from 'jscodeshift'; + +import { migrateVariantProps } from './utils/variant-migration.js'; + +// eslint-disable-next-line no-restricted-exports -- jscodeshift requires default export +export default function transformer(file: FileInfo, api: API, options: Options) { + return migrateVariantProps(file, api, options, { + componentNames: ['Button', 'IconButton'], + variantMapping: { + tertiary: 'inverse', + foregroundMuted: 'secondary', + }, + transformLabel: 'button-variant-mapping', + includesTertiarySemanticWarning: true, + }); +} diff --git a/packages/migrator/src/v8-to-v9/transforms/example-transform.ts b/packages/migrator/src/v8-to-v9/transforms/example-transform.ts new file mode 100644 index 000000000..706d1a089 --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/example-transform.ts @@ -0,0 +1,92 @@ +/** + * Example Transform + * + * This is a template/example transform showing how to write codemods for CDS migrations. + * You can use this as a starting point for creating actual migration transforms. + */ + +import type { API, FileInfo, Options } from 'jscodeshift'; + +import { addTodoToAttribute, getLogger, hasMigrationTodo } from '../../utils/index.js'; + +// eslint-disable-next-line no-restricted-exports -- jscodeshift requires default export +export default function transformer(file: FileInfo, api: API, options: Options) { + const j = api.jscodeshift; + const root = j(file.source); + const logger = getLogger(); + + let hasChanges = false; + + // Example 1: Rename a component + root + .find(j.JSXElement) + .filter((path) => { + const openingElement = path.value.openingElement; + return j.JSXIdentifier.check(openingElement.name) && openingElement.name.name === 'OldButton'; + }) + .forEach((path) => { + // Rename component + const openingElement = path.value.openingElement; + const closingElement = path.value.closingElement; + + if (j.JSXIdentifier.check(openingElement.name)) { + openingElement.name.name = 'Button'; + hasChanges = true; + + logger?.success('Renamed OldButton to Button', file.path, path.value.loc?.start.line); + } + + if (closingElement && j.JSXIdentifier.check(closingElement.name)) { + closingElement.name.name = 'Button'; + } + }); + + // Example 2: Add TODO for complex migrations + root.find(j.JSXAttribute, { name: { name: 'deprecatedProp' } }).forEach((path) => { + if (!hasMigrationTodo(path.parent)) { + addTodoToAttribute(j, path, { + message: "The 'deprecatedProp' has been removed in v9", + context: + 'Please migrate to the new API. See: https://docs.coinbase.com/cds/migration-guide', + }); + + logger?.todo( + 'Manual migration required for deprecatedProp', + file.path, + path.value.loc?.start.line, + 'This prop has breaking changes that need manual review', + ); + + hasChanges = true; + } + }); + + // Example 3: Update import statements + root + .find(j.ImportDeclaration) + .filter((path) => path.value.source.value === '@coinbase/cds-web') + .forEach((path) => { + path.value.specifiers?.forEach((specifier) => { + if (j.ImportSpecifier.check(specifier) && specifier.imported.name === 'OldButton') { + specifier.imported.name = 'Button'; + if (specifier.local) { + specifier.local.name = 'Button'; + } + hasChanges = true; + + logger?.success( + 'Updated import: OldButton β†’ Button', + file.path, + path.value.loc?.start.line, + ); + } + }); + }); + + // Return null if no changes to skip writing the file + if (!hasChanges) { + return null; + } + + return root.toSource(); +} diff --git a/packages/migrator/src/v8-to-v9/transforms/test-utils.ts b/packages/migrator/src/v8-to-v9/transforms/test-utils.ts new file mode 100644 index 000000000..768c145c4 --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/test-utils.ts @@ -0,0 +1,23 @@ +import fs from 'fs'; +import type { API, FileInfo, Options } from 'jscodeshift'; +import { withParser } from 'jscodeshift'; +import os from 'os'; +import path from 'path'; + +import type { LogEntry } from '../../utils/logger.js'; +import { createLogger } from '../../utils/logger.js'; + +export function runTransform( + transform: (file: FileInfo, api: API, options: Options) => string | null, + source: string, +): { output: string; entries: LogEntry[] } { + const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cds-migrator-test-')); + const logger = createLogger(outputDir); + const api = { jscodeshift: withParser('tsx') } as API; + + const result = transform({ path: '/tmp/example.tsx', source }, api, {}); + return { + output: result ?? source, + entries: logger.getEntries(), + }; +} diff --git a/packages/migrator/src/v8-to-v9/transforms/utils/variant-migration.ts b/packages/migrator/src/v8-to-v9/transforms/utils/variant-migration.ts new file mode 100644 index 000000000..84b08e1ab --- /dev/null +++ b/packages/migrator/src/v8-to-v9/transforms/utils/variant-migration.ts @@ -0,0 +1,362 @@ +import type { + API, + ASTPath, + Collection, + FileInfo, + JSXAttribute, + JSXOpeningElement, + Options, +} from 'jscodeshift'; + +import { + addTodoToAttribute, + getLogger, + hasMigrationTodo, + insertTodoComment, +} from '../../../utils/index.js'; + +type VariantMigrationOptions = { + componentNames: string[]; + variantMapping: Record; + transformLabel: string; + includesTertiarySemanticWarning?: boolean; +}; + +type ComponentImports = { + localToImported: Map; + localNames: Set; +}; + +function toSourceValue(source: string | null | undefined): string { + return typeof source === 'string' ? source : ''; +} + +function collectComponentImports( + root: Collection, + j: API['jscodeshift'], + componentNames: string[], +): ComponentImports { + const targetNames = new Set(componentNames); + const localToImported = new Map(); + const localNames = new Set(); + + root + .find(j.ImportDeclaration) + .filter((path: any) => toSourceValue(path.value.source.value).startsWith('@coinbase/cds')) + .forEach((path: any) => { + path.value.specifiers?.forEach((specifier: any) => { + if (!j.ImportSpecifier.check(specifier)) { + return; + } + + const importedName = specifier.imported.name; + if (!targetNames.has(importedName)) { + return; + } + + const localName = specifier.local?.name ?? importedName; + localToImported.set(localName, importedName); + localNames.add(localName); + }); + }); + + return { localToImported, localNames }; +} + +function getOpeningElementName( + j: API['jscodeshift'], + openingElement: JSXOpeningElement, +): string | null { + if (j.JSXIdentifier.check(openingElement.name)) { + return openingElement.name.name; + } + + return null; +} + +function getAttributeName(j: API['jscodeshift'], attribute: any): string | null { + if (!j.JSXAttribute.check(attribute) || !j.JSXIdentifier.check(attribute.name)) { + return null; + } + + return attribute.name.name; +} + +function createTodoReporter( + j: API['jscodeshift'], + file: FileInfo, + label: string, + openingPath: ASTPath, + fallbackLine?: number, +) { + const logger = getLogger(); + const openingName = getOpeningElementName(j, openingPath.value) ?? 'Component'; + + const hasExistingTodo = () => { + if (hasMigrationTodo(openingPath)) { + return true; + } + + const parentPath = openingPath.parent; + if (parentPath && hasMigrationTodo(parentPath as any)) { + return true; + } + + return false; + }; + + return (attributePath: ASTPath | null, message: string, details: string) => { + if (!hasExistingTodo()) { + if (attributePath) { + addTodoToAttribute(j, attributePath, { message, context: details }); + } else { + insertTodoComment(j, openingPath, { message, context: details }); + } + } + + logger?.todo(`[${label}] ${message}`, file.path, fallbackLine, `${openingName}: ${details}`); + }; +} + +function unwrapObjectExpression(j: API['jscodeshift'], value: any): any | null { + if (!value) { + return null; + } + + if (j.ObjectExpression.check(value)) { + return value; + } + + if ( + (j.TSAsExpression.check(value) || j.TSTypeAssertion.check(value)) && + j.ObjectExpression.check(value.expression) + ) { + return value.expression; + } + + return null; +} + +function getObjectVariantProperty(j: API['jscodeshift'], objectExpression: any): any | null { + const properties = objectExpression.properties ?? []; + for (const property of properties) { + const isProperty = j.Property.check(property); + const isObjectProperty = 'ObjectProperty' in j && (j as any).ObjectProperty.check(property); + if (!isProperty && !isObjectProperty) { + continue; + } + + if (j.Identifier.check(property.key) && property.key.name === 'variant') { + return property; + } + + if ( + (j.StringLiteral.check(property.key) || j.Literal.check(property.key)) && + property.key.value === 'variant' + ) { + return property; + } + } + + return null; +} + +function remapStringVariantValue( + j: API['jscodeshift'], + value: any, + variantMapping: Record, +): { didChange: boolean; previous?: string; next?: string } { + if (!value) { + return { didChange: false }; + } + + if ((j.TSAsExpression.check(value) || j.TSTypeAssertion.check(value)) && value.expression) { + const expressionResult = remapStringVariantValue(j, value.expression, variantMapping); + if (expressionResult.didChange) { + return expressionResult; + } + } + + if (j.StringLiteral.check(value) || j.Literal.check(value)) { + const previous = String(value.value); + const next = variantMapping[previous]; + if (next) { + value.value = next; + return { didChange: true, previous, next }; + } + return { didChange: false }; + } + + if (j.JSXExpressionContainer.check(value)) { + const expression = value.expression; + if (j.StringLiteral.check(expression) || j.Literal.check(expression)) { + const previous = String(expression.value); + const next = variantMapping[previous]; + if (next) { + expression.value = next; + return { didChange: true, previous, next }; + } + } + } + + return { didChange: false }; +} + +function expressionMentionsVariant(expression: any, variant: string): boolean { + const visited = new WeakSet(); + const stack: any[] = [expression]; + + while (stack.length > 0) { + const current = stack.pop(); + if (!current || typeof current !== 'object') { + continue; + } + if (visited.has(current)) { + continue; + } + visited.add(current); + + if ('value' in current && current.value === variant) { + return true; + } + + for (const child of Object.values(current)) { + if (child && typeof child === 'object') { + stack.push(child); + } + } + } + + return false; +} + +export function migrateVariantProps( + file: FileInfo, + api: API, + options: Options, + migrationOptions: VariantMigrationOptions, +): string | null { + const { + componentNames, + variantMapping, + transformLabel, + includesTertiarySemanticWarning = false, + } = migrationOptions; + const j = api.jscodeshift; + const root = j(file.source); + const logger = getLogger(); + + const imports = collectComponentImports(root, j, componentNames); + if (imports.localNames.size === 0) { + return null; + } + + let hasChanges = false; + + const variableDeclaratorsByName = new Map(); + root.find(j.VariableDeclarator).forEach((path: any) => { + if (j.Identifier.check(path.value.id)) { + variableDeclaratorsByName.set(path.value.id.name, path.value); + } + }); + + root + .find(j.JSXOpeningElement) + .filter((path: any) => { + const localName = getOpeningElementName(j, path.value); + return localName ? imports.localNames.has(localName) : false; + }) + .forEach((openingPath: any) => { + const localName = getOpeningElementName(j, openingPath.value); + if (!localName) { + return; + } + + const importedName = imports.localToImported.get(localName) ?? localName; + const fallbackLine = openingPath.value.loc?.start.line; + const reportTodo = createTodoReporter(j, file, transformLabel, openingPath, fallbackLine); + + openingPath.value.attributes.forEach((attribute: any) => { + if (j.JSXAttribute.check(attribute) && getAttributeName(j, attribute) === 'variant') { + const attributePath = j(openingPath) + .find(j.JSXAttribute, { name: { name: 'variant' } }) + .at(0) + .paths()[0]; + if (!attributePath) { + return; + } + + const remapResult = remapStringVariantValue(j, attribute.value, variantMapping); + if (remapResult.didChange) { + hasChanges = true; + logger?.success( + `[${transformLabel}] Updated ${importedName} variant: ${remapResult.previous} -> ${remapResult.next}`, + file.path, + attribute.loc?.start.line, + ); + return; + } + + if (!attribute.value || j.JSXExpressionContainer.check(attribute.value)) { + const expression = j.JSXExpressionContainer.check(attribute.value) + ? attribute.value.expression + : null; + const containsTertiary = expression + ? expressionMentionsVariant(expression, 'tertiary') + : false; + const tertiaryContext = + containsTertiary && includesTertiarySemanticWarning + ? ' Dynamic tertiary values need intent review: v9 tertiary no longer matches v8 tertiary visuals; use inverse if old v8 look is intended.' + : ''; + + reportTodo( + attributePath, + `${importedName} variant requires manual migration`, + `Found dynamic or non-literal variant value.${tertiaryContext} Verify mapping to v9 variants and update manually if needed.`, + ); + hasChanges = true; + } + } + + if (!j.JSXSpreadAttribute.check(attribute)) { + return; + } + + const spreadArgument = attribute.argument; + let didRewriteObjectVariant = false; + + if (j.Identifier.check(spreadArgument)) { + const declarator = variableDeclaratorsByName.get(spreadArgument.name); + const objectExpression = unwrapObjectExpression(j, declarator?.init); + if (objectExpression) { + const variantProperty = getObjectVariantProperty(j, objectExpression); + if (variantProperty) { + const remapResult = remapStringVariantValue(j, variantProperty.value, variantMapping); + if (remapResult.didChange) { + didRewriteObjectVariant = true; + hasChanges = true; + logger?.success( + `[${transformLabel}] Updated spread object variant for ${importedName}: ${remapResult.previous} -> ${remapResult.next}`, + file.path, + variantProperty.loc?.start.line ?? attribute.loc?.start.line, + ); + } + } + } + } + + reportTodo( + null, + `${importedName} spread props need manual review`, + `Found spread props on ${importedName}. Spread values may still contain legacy variant values. Review spread source and flatten/update variant explicitly.${didRewriteObjectVariant ? ' An obvious local object-literal variant was auto-updated, but full spread safety still requires manual verification.' : ''}`, + ); + hasChanges = true; + }); + }); + + if (!hasChanges) { + return null; + } + + return root.toSource(options); +} diff --git a/packages/migrator/tsconfig.build.json b/packages/migrator/tsconfig.build.json new file mode 100644 index 000000000..a227a333b --- /dev/null +++ b/packages/migrator/tsconfig.build.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "sourceMap": false + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "**/__stories__/**", + "**/__tests__/**", + "**/__mocks__/**", + "**/__fixtures__/**", + "**/*.stories.*", + "**/*.test.*", + "**/*.spec.*" + ], + "references": [] +} diff --git a/packages/migrator/tsconfig.json b/packages/migrator/tsconfig.json new file mode 100644 index 000000000..53e3c6f11 --- /dev/null +++ b/packages/migrator/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.project.json", + "compilerOptions": { + "declarationDir": "dts", + "rootDir": "src" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + // Keep migrator package typecheck focused on implementation source only. + // Transform fixtures intentionally contain example app code patterns that + // are validated by Jest transform tests, not by tsc project compilation. + "**/__tests__/**", + "**/__fixtures__/**", + "**/*.test.*", + "**/*.spec.*" + ], + "references": [] +} diff --git a/packages/migrator/tsconfig.spec.json b/packages/migrator/tsconfig.spec.json new file mode 100644 index 000000000..0a6eba130 --- /dev/null +++ b/packages/migrator/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [ + "jest", + "node" + ] + }, + "include": [ + "src/**/*.test.ts", + "src/**/*.test.tsx", + "src/**/*.spec.ts", + "src/**/*.spec.tsx", + "src/**/__tests__/**/*.ts", + "src/**/__tests__/**/*.tsx" + ] +} diff --git a/yarn.lock b/yarn.lock index 1f6a5146e..6c0eb1485 100644 --- a/yarn.lock +++ b/yarn.lock @@ -281,7 +281,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.15.5, @babel/core@npm:^7.18.2, @babel/core@npm:^7.18.5, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.23.2, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.25.9, @babel/core@npm:^7.28.0, @babel/core@npm:^7.29.0": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.15.5, @babel/core@npm:^7.18.2, @babel/core@npm:^7.18.5, @babel/core@npm:^7.20.0, @babel/core@npm:^7.21.3, @babel/core@npm:^7.23.0, @babel/core@npm:^7.23.2, @babel/core@npm:^7.24.4, @babel/core@npm:^7.25.2, @babel/core@npm:^7.25.9, @babel/core@npm:^7.28.0, @babel/core@npm:^7.29.0": version: 7.29.0 resolution: "@babel/core@npm:7.29.0" dependencies: @@ -1165,7 +1165,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-flow-strip-types@npm:^7.25.2": +"@babel/plugin-transform-flow-strip-types@npm:^7.25.2, @babel/plugin-transform-flow-strip-types@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-flow-strip-types@npm:7.27.1" dependencies: @@ -1258,7 +1258,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.12.13, @babel/plugin-transform-modules-commonjs@npm:^7.18.2, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1": +"@babel/plugin-transform-modules-commonjs@npm:^7.12.13, @babel/plugin-transform-modules-commonjs@npm:^7.18.2, @babel/plugin-transform-modules-commonjs@npm:^7.23.0, @babel/plugin-transform-modules-commonjs@npm:^7.24.8, @babel/plugin-transform-modules-commonjs@npm:^7.27.1": version: 7.28.6 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6" dependencies: @@ -1319,7 +1319,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.0.0-0, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.22.11, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.24.7, @babel/plugin-transform-nullish-coalescing-operator@npm:^7.27.1": version: 7.28.6 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.28.6" dependencies: @@ -1379,7 +1379,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.0.0-0, @babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.27.1": +"@babel/plugin-transform-optional-chaining@npm:^7.0.0-0, @babel/plugin-transform-optional-chaining@npm:^7.23.0, @babel/plugin-transform-optional-chaining@npm:^7.24.8, @babel/plugin-transform-optional-chaining@npm:^7.27.1": version: 7.28.6 resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.6" dependencies: @@ -1402,7 +1402,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.24.7, @babel/plugin-transform-private-methods@npm:^7.27.1": +"@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.24.7, @babel/plugin-transform-private-methods@npm:^7.27.1": version: 7.28.6 resolution: "@babel/plugin-transform-private-methods@npm:7.28.6" dependencies: @@ -1768,6 +1768,19 @@ __metadata: languageName: node linkType: hard +"@babel/preset-flow@npm:^7.22.15": + version: 7.27.1 + resolution: "@babel/preset-flow@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-validator-option": "npm:^7.27.1" + "@babel/plugin-transform-flow-strip-types": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/252216c91ba3cc126f10c81c1df495ef2c622687d17373bc619354a7fb7280ea83f434ed1e7149dbddd712790d16ab60f5b864d007edd153931d780f834e52c1 + languageName: node + linkType: hard + "@babel/preset-modules@npm:0.1.6-no-external-plugins": version: 0.1.6-no-external-plugins resolution: "@babel/preset-modules@npm:0.1.6-no-external-plugins" @@ -1827,6 +1840,21 @@ __metadata: languageName: node linkType: hard +"@babel/register@npm:^7.22.15": + version: 7.28.6 + resolution: "@babel/register@npm:7.28.6" + dependencies: + clone-deep: "npm:^4.0.1" + find-cache-dir: "npm:^2.0.0" + make-dir: "npm:^2.1.0" + pirates: "npm:^4.0.6" + source-map-support: "npm:^0.5.16" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/372380504970cf7654c2d65e09c34ed0b3217da64cb0edb6376a89eba7b603c8bdaba666eead7dcd6ed21badd52d396c2c0d6f914ae4dc6c9009e3d03d260e98 + languageName: node + linkType: hard + "@babel/runtime-corejs3@npm:^7.10.2, @babel/runtime-corejs3@npm:^7.25.9": version: 7.26.0 resolution: "@babel/runtime-corejs3@npm:7.26.0" @@ -2187,6 +2215,27 @@ __metadata: languageName: unknown linkType: soft +"@coinbase/cds-migrator@workspace:^, @coinbase/cds-migrator@workspace:packages/migrator": + version: 0.0.0-use.local + resolution: "@coinbase/cds-migrator@workspace:packages/migrator" + dependencies: + "@babel/core": "npm:^7.28.0" + "@babel/preset-env": "npm:^7.28.0" + "@babel/preset-react": "npm:^7.28.5" + "@babel/preset-typescript": "npm:^7.27.1" + "@types/inquirer": "npm:^9.0.7" + "@types/jscodeshift": "npm:^0.11.10" + "@types/node": "npm:^20.0.0" + commander: "npm:^11.1.0" + inquirer: "npm:^9.2.12" + jscodeshift: "npm:^0.15.1" + bin: + cds-migrate: ./esm/cli.js + "cds-migrate:all": ./esm/bin/migrate-all.js + "cds-migrate:clear-history": ./esm/bin/clear-history.js + languageName: unknown + linkType: soft + "@coinbase/cds-mobile-visualization@workspace:^, @coinbase/cds-mobile-visualization@workspace:packages/mobile-visualization": version: 0.0.0-use.local resolution: "@coinbase/cds-mobile-visualization@workspace:packages/mobile-visualization" @@ -5445,18 +5494,18 @@ __metadata: languageName: node linkType: hard -"@inquirer/external-editor@npm:^1.0.1": - version: 1.0.1 - resolution: "@inquirer/external-editor@npm:1.0.1" +"@inquirer/external-editor@npm:^1.0.1, @inquirer/external-editor@npm:^1.0.2": + version: 1.0.3 + resolution: "@inquirer/external-editor@npm:1.0.3" dependencies: - chardet: "npm:^2.1.0" - iconv-lite: "npm:^0.6.3" + chardet: "npm:^2.1.1" + iconv-lite: "npm:^0.7.0" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10c0/bdac4395e0bba7065d39b141d618bfc06369f246c402c511396a5238baf2657f3038ccba8438521a49e5cb602f4302b9d1f46b52b647b27af2c9911720022118 + checksum: 10c0/82951cb7f3762dd78cca2ea291396841e3f4adfe26004b5badfed1cec4b6a04bb567dff94d0e41b35c61bdd7957317c64c22f58074d14b238d44e44d9e420019 languageName: node linkType: hard @@ -5467,6 +5516,13 @@ __metadata: languageName: node linkType: hard +"@inquirer/figures@npm:^1.0.3": + version: 1.0.15 + resolution: "@inquirer/figures@npm:1.0.15" + checksum: 10c0/6e39a040d260ae234ae220180b7994ff852673e20be925f8aa95e78c7934d732b018cbb4d0ec39e600a410461bcb93dca771e7de23caa10630d255692e440f69 + languageName: node + linkType: hard + "@inquirer/input@npm:^4.2.2": version: 4.2.2 resolution: "@inquirer/input@npm:4.2.2" @@ -9679,6 +9735,16 @@ __metadata: languageName: node linkType: hard +"@types/inquirer@npm:^9.0.7": + version: 9.0.9 + resolution: "@types/inquirer@npm:9.0.9" + dependencies: + "@types/through": "npm:*" + rxjs: "npm:^7.2.0" + checksum: 10c0/235a02a3afa5b238ca9093ef7064e17d763ba134b1afd5a263ffc363ccdb6d1f7d64aa3866ca93e7ad52be4ff21324368a983d20d35caf21c16475cfb32db8c8 + languageName: node + linkType: hard + "@types/intl@npm:^1.2.0": version: 1.2.0 resolution: "@types/intl@npm:1.2.0" @@ -9765,6 +9831,16 @@ __metadata: languageName: node linkType: hard +"@types/jscodeshift@npm:^0.11.10": + version: 0.11.11 + resolution: "@types/jscodeshift@npm:0.11.11" + dependencies: + ast-types: "npm:^0.14.1" + recast: "npm:^0.20.3" + checksum: 10c0/b3d2be46d523ae679a2c986d7f98232aabaa761c960423105286bfd682fb57f9366f6afed1e1d6b35e4923b7e038c0aa539032d7e7fd430754683078032cd578 + languageName: node + linkType: hard + "@types/jsdom@npm:^20.0.0": version: 20.0.0 resolution: "@types/jsdom@npm:20.0.0" @@ -10161,6 +10237,15 @@ __metadata: languageName: node linkType: hard +"@types/through@npm:*": + version: 0.0.33 + resolution: "@types/through@npm:0.0.33" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/6a8edd7f40cd7e197318e86310a40e568cddd380609dde59b30d5cc6c5f8276ddc698905eac4b3b429eb39f2e8ee326bc20dc6e95a2cdc41c4d3fc9a1ebd4929 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.2 resolution: "@types/tough-cookie@npm:4.0.2" @@ -11775,6 +11860,15 @@ __metadata: languageName: node linkType: hard +"ast-types@npm:0.14.2, ast-types@npm:^0.14.1": + version: 0.14.2 + resolution: "ast-types@npm:0.14.2" + dependencies: + tslib: "npm:^2.0.1" + checksum: 10c0/5d66d89b6c07fe092087454b6042dbaf81f2882b176db93861e2b986aafe0bce49e1f1ff59aac775d451c1426ad1e967d250e9e3548f5166ea8a3475e66c169d + languageName: node + linkType: hard + "ast-types@npm:^0.13.4": version: 0.13.4 resolution: "ast-types@npm:0.13.4" @@ -11913,6 +12007,15 @@ __metadata: languageName: node linkType: hard +"babel-core@npm:^7.0.0-bridge.0": + version: 7.0.0-bridge.0 + resolution: "babel-core@npm:7.0.0-bridge.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/f57576e30267be4607d163b7288031d332cf9200ea35efe9fb33c97f834e304376774c28c1f9d6928d6733fcde7041e4010f1248a0519e7730c590d4b07b9608 + languageName: node + linkType: hard + "babel-jest@npm:^29.7.0": version: 29.7.0 resolution: "babel-jest@npm:29.7.0" @@ -13060,10 +13163,10 @@ __metadata: languageName: node linkType: hard -"chardet@npm:^2.1.0": - version: 2.1.0 - resolution: "chardet@npm:2.1.0" - checksum: 10c0/d1b03e47371851ed72741a898281d58f8a9b577aeea6fdfa75a86832898b36c550b3ad057e66d50d774a9cebd9f56c66b6880e4fe75e387794538ba7565b0b6f +"chardet@npm:^2.1.1": + version: 2.1.1 + resolution: "chardet@npm:2.1.1" + checksum: 10c0/d8391dd412338442b3de0d3a488aa9327f8bcf74b62b8723d6bd0b85c4084d50b731320e0a7c710edb1d44de75969995d2784b80e4c13b004a6c7a0db4c6e793 languageName: node linkType: hard @@ -14366,9 +14469,9 @@ __metadata: linkType: hard "csstype@npm:^3.0.2, csstype@npm:^3.1.3": - version: 3.1.3 - resolution: "csstype@npm:3.1.3" - checksum: 10c0/80c089d6f7e0c5b2bd83cf0539ab41474198579584fa10d86d0cafe0642202343cbc119e076a0b1aece191989477081415d66c9fefbf3c957fc2fc4b7009f248 + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce languageName: node linkType: hard @@ -17944,6 +18047,13 @@ __metadata: languageName: node linkType: hard +"flow-parser@npm:0.*": + version: 0.300.0 + resolution: "flow-parser@npm:0.300.0" + checksum: 10c0/c95b39dfbbfed8fc2e09363e895e7d01f453f08a891c057cf74826f33dc7a7ab731b99065216f9c2324df03de88a824f5c382881a8da93bbed1249a15d2bed83 + languageName: node + linkType: hard + "follow-redirects@npm:1.5.10": version: 1.5.10 resolution: "follow-redirects@npm:1.5.10" @@ -19580,6 +19690,15 @@ __metadata: languageName: node linkType: hard +"iconv-lite@npm:^0.7.0": + version: 0.7.2 + resolution: "iconv-lite@npm:0.7.2" + dependencies: + safer-buffer: "npm:>= 2.1.2 < 3.0.0" + checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 + languageName: node + linkType: hard + "icss-utils@npm:^5.0.0, icss-utils@npm:^5.1.0": version: 5.1.0 resolution: "icss-utils@npm:5.1.0" @@ -19799,6 +19918,26 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:^9.2.12": + version: 9.3.8 + resolution: "inquirer@npm:9.3.8" + dependencies: + "@inquirer/external-editor": "npm:^1.0.2" + "@inquirer/figures": "npm:^1.0.3" + ansi-escapes: "npm:^4.3.2" + cli-width: "npm:^4.1.0" + mute-stream: "npm:1.0.0" + ora: "npm:^5.4.1" + run-async: "npm:^3.0.0" + rxjs: "npm:^7.8.1" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wrap-ansi: "npm:^6.2.0" + yoctocolors-cjs: "npm:^2.1.2" + checksum: 10c0/f9e64487413816460d2eb04520cd0898b8d488533bba93dfb432013383fe7bab5ddffd9ecfe5d5e2d96aaac86086bfa13c0a397a75083896693ab9d36177197b + languageName: node + linkType: hard + "internal-slot@npm:^1.1.0": version: 1.1.0 resolution: "internal-slot@npm:1.1.0" @@ -21485,6 +21624,41 @@ __metadata: languageName: node linkType: hard +"jscodeshift@npm:^0.15.1": + version: 0.15.2 + resolution: "jscodeshift@npm:0.15.2" + dependencies: + "@babel/core": "npm:^7.23.0" + "@babel/parser": "npm:^7.23.0" + "@babel/plugin-transform-class-properties": "npm:^7.22.5" + "@babel/plugin-transform-modules-commonjs": "npm:^7.23.0" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.22.11" + "@babel/plugin-transform-optional-chaining": "npm:^7.23.0" + "@babel/plugin-transform-private-methods": "npm:^7.22.5" + "@babel/preset-flow": "npm:^7.22.15" + "@babel/preset-typescript": "npm:^7.23.0" + "@babel/register": "npm:^7.22.15" + babel-core: "npm:^7.0.0-bridge.0" + chalk: "npm:^4.1.2" + flow-parser: "npm:0.*" + graceful-fs: "npm:^4.2.4" + micromatch: "npm:^4.0.4" + neo-async: "npm:^2.5.0" + node-dir: "npm:^0.1.17" + recast: "npm:^0.23.3" + temp: "npm:^0.8.4" + write-file-atomic: "npm:^2.3.0" + peerDependencies: + "@babel/preset-env": ^7.1.6 + peerDependenciesMeta: + "@babel/preset-env": + optional: true + bin: + jscodeshift: bin/jscodeshift.js + checksum: 10c0/79afb059b9ca92712af02bdc8d6ff144de7aaf5e2cdcc6f6534e7a86a7347b0a278d9f4884f2c78dac424162a353aafff183a60e868f71132be2c5b5304aeeb8 + languageName: node + linkType: hard + "jsdom@npm:^20.0.0": version: 20.0.0 resolution: "jsdom@npm:20.0.0" @@ -23819,7 +23993,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:2 || 3, minimatch@npm:3.1.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:2 || 3, minimatch@npm:3.1.2, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -24174,6 +24348,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:1.0.0": + version: 1.0.0 + resolution: "mute-stream@npm:1.0.0" + checksum: 10c0/dce2a9ccda171ec979a3b4f869a102b1343dee35e920146776780de182f16eae459644d187e38d59a3d37adf85685e1c17c38cf7bfda7e39a9880f7a1d10a74c + languageName: node + linkType: hard + "mute-stream@npm:^2.0.0": version: 2.0.0 resolution: "mute-stream@npm:2.0.0" @@ -24260,7 +24441,7 @@ __metadata: languageName: node linkType: hard -"neo-async@npm:^2.6.2": +"neo-async@npm:^2.5.0, neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" checksum: 10c0/c2f5a604a54a8ec5438a342e1f356dff4bc33ccccdb6dc668d94fe8e5eccfc9d2c2eea6064b0967a767ba63b33763f51ccf2cd2441b461a7322656c1f06b3f5d @@ -24307,6 +24488,15 @@ __metadata: languageName: node linkType: hard +"node-dir@npm:^0.1.17": + version: 0.1.17 + resolution: "node-dir@npm:0.1.17" + dependencies: + minimatch: "npm:^3.0.2" + checksum: 10c0/16222e871708c405079ff8122d4a7e1d522c5b90fc8f12b3112140af871cfc70128c376e845dcd0044c625db0d2efebd2d852414599d240564db61d53402b4c1 + languageName: node + linkType: hard + "node-emoji@npm:^2.1.0": version: 2.2.0 resolution: "node-emoji@npm:2.2.0" @@ -27791,7 +27981,19 @@ __metadata: languageName: node linkType: hard -"recast@npm:^0.23.5": +"recast@npm:^0.20.3": + version: 0.20.5 + resolution: "recast@npm:0.20.5" + dependencies: + ast-types: "npm:0.14.2" + esprima: "npm:~4.0.0" + source-map: "npm:~0.6.1" + tslib: "npm:^2.0.1" + checksum: 10c0/7810216ff36c7376eddd66d3ce6b2df421305fdc983f2122711837911712177d52d804419655e1f29d4bb93016c178cffe442af410bdcf726050ca19af6fed32 + languageName: node + linkType: hard + +"recast@npm:^0.23.3, recast@npm:^0.23.5": version: 0.23.11 resolution: "recast@npm:0.23.11" dependencies: @@ -28625,6 +28827,13 @@ __metadata: languageName: node linkType: hard +"run-async@npm:^3.0.0": + version: 3.0.0 + resolution: "run-async@npm:3.0.0" + checksum: 10c0/b18b562ae37c3020083dcaae29642e4cc360c824fbfb6b7d50d809a9d5227bb986152d09310255842c8dce40526e82ca768f02f00806c91ba92a8dfa6159cb85 + languageName: node + linkType: hard + "run-async@npm:^4.0.5": version: 4.0.6 resolution: "run-async@npm:4.0.6" @@ -28650,7 +28859,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.8.2": +"rxjs@npm:^7.2.0, rxjs@npm:^7.8.1, rxjs@npm:^7.8.2": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -30663,6 +30872,15 @@ __metadata: languageName: node linkType: hard +"temp@npm:^0.8.4": + version: 0.8.4 + resolution: "temp@npm:0.8.4" + dependencies: + rimraf: "npm:~2.6.2" + checksum: 10c0/7f071c963031bfece37e13c5da11e9bb451e4ddfc4653e23e327a2f91594102dc826ef6a693648e09a6e0eb856f507967ec759ae55635e0878091eccf411db37 + languageName: node + linkType: hard + "temp@npm:^0.9.4": version: 0.9.4 resolution: "temp@npm:0.9.4" @@ -32009,6 +32227,7 @@ __metadata: "@coinbase/cds-common": "workspace:^" "@coinbase/cds-icons": "workspace:^" "@coinbase/cds-illustrations": "workspace:^" + "@coinbase/cds-migrator": "workspace:^" "@coinbase/cds-web": "workspace:^" "@coinbase/cds-web-visualization": "workspace:^" "@types/react": "npm:19.1.2" @@ -32834,6 +33053,17 @@ __metadata: languageName: node linkType: hard +"write-file-atomic@npm:^2.3.0": + version: 2.4.3 + resolution: "write-file-atomic@npm:2.4.3" + dependencies: + graceful-fs: "npm:^4.1.11" + imurmurhash: "npm:^0.1.4" + signal-exit: "npm:^3.0.2" + checksum: 10c0/8cb4bba0c1ab814a9b127844da0db4fb8c5e06ddbe6317b8b319377c73b283673036c8b9360120062898508b9428d81611cf7fa97584504a00bc179b2a580b92 + languageName: node + linkType: hard + "write-file-atomic@npm:^3.0.3": version: 3.0.3 resolution: "write-file-atomic@npm:3.0.3"