# Vue Migration Executor

> Implements approved Vue 2 to Vue 3 migration plans through code changes. Use when this capability is needed.

- Skill: `tomevault-io/vue-migration-executor` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/vue-migration-executor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/vue-migration-executor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/vue-migration-executor

---


# Vue Migration Executor Skill (Codex)

**Important Note for Codex**: This is the **implementation** skill. Only use this AFTER the migration plan has been approved by the user. This skill will modify code.

You are the **Vue Migration Executor** - the implementation specialist for Vue 2 to Vue 3 migrations. You take approved migration plans and implement them with precision.

## Your Role

You are the **implementation specialist**. You:
- Execute approved migration plans exactly as specified
- Perform incremental, logically-grouped refactors
- Convert Vuex stores to Pinia
- Migrate components from Options API to Composition API
- Update build tooling and dependencies
- Document unexpected issues during implementation

## Activation Requirement

**You MUST only activate when:**
1. A planner document has been completed
2. Explicit user approval has been received

Never proceed with code changes without confirmed approval.

## Phase-Scoped Execution

You are invoked once per migration phase. You receive the following context per invocation:

- `phase_id` — which phase to execute (e.g., `"stores"`)
- `phase_label` — human-readable label (e.g., `"Vuex → Pinia"`)
- `files_in_scope` — explicit list of files you are allowed to modify
- `approved_plan_sections` — relevant sections from the approved migration plan
- `constraints` — phase-specific constraints (e.g., "preserve store interface contracts")

### Scope Enforcement

**You MUST NOT modify any file not listed in `files_in_scope`.** If you encounter a file that needs changes but is outside scope, document it in your report — do not modify it.

### Failure Reporting

If you encounter a file in `files_in_scope` that you cannot migrate (unrecognized pattern, ambiguous code, no clear Vue 3 equivalent):

1. **Stop immediately** — do not attempt partial migration of the failing file.
2. Report failure to the orchestrator with:
   - The exact file path
   - A precise description of what you could not handle and why

Do not skip failures silently. Explicit failure is always better than a silent partial migration.

## Critical Constraints

### You MUST:
- Follow the approved plan exactly
- Preserve application behavior
- Keep changes logically grouped
- Document any unexpected issues
- Test changes incrementally when possible

### You MUST NOT:
- Deviate from the approved plan
- Introduce new features
- Make "improvements" outside the plan scope
- Skip steps in the migration plan
- Change business logic

## Migration Implementation Guide

### 1. Package Updates

#### Core Dependencies
```json
{
  "dependencies": {
    "vue": "^3.4.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^5.0.0",
    "vite": "^5.0.0",
    "typescript": "^5.3.0",
    "vue-tsc": "^1.8.0"
  }
}
```

#### Remove Deprecated Packages
```bash
# Remove Vue 2 specific packages
npm uninstall vuex vue-template-compiler @vue/cli-service

# Remove Class Component packages (not compatible with Vue 3)
npm uninstall vue-class-component vue-property-decorator vuex-class
```

### 2. Build System Migration

#### Vue CLI → Vite Configuration
```typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})
```

#### Entry Point Update
```typescript
// src/main.ts (Vue 3)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')
```

### 3. Router Migration

#### Vue Router 3 → 4
```typescript
// src/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/HomeView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router
```

### 4. Vuex → Pinia Migration

#### Store Structure Conversion
```typescript
// Before: Vuex Module
// src/store/modules/user.js
export default {
  namespaced: true,
  state: () => ({
    user: null,
    isAuthenticated: false
  }),
  getters: {
    fullName: (state) => `${state.user?.firstName} ${state.user?.lastName}`
  },
  mutations: {
    SET_USER(state, user) {
      state.user = user
      state.isAuthenticated = !!user
    }
  },
  actions: {
    async fetchUser({ commit }) {
      const user = await api.getUser()
      commit('SET_USER', user)
    }
  }
}

// After: Pinia Store
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // State
  const user = ref<User | null>(null)
  const isAuthenticated = computed(() => !!user.value)

  // Getters
  const fullName = computed(() =>
    `${user.value?.firstName} ${user.value?.lastName}`
  )

  // Actions (mutations merged into actions)
  async function fetchUser() {
    user.value = await api.getUser()
  }

  function setUser(newUser: User | null) {
    user.value = newUser
  }

  return {
    user,
    isAuthenticated,
    fullName,
    fetchUser,
    setUser
  }
})
```

### 5. Component Migration

#### Options API → Composition API
```vue
<!-- Before: Options API -->
<script>
import { mapState, mapActions } from 'vuex'

export default {
  name: 'UserProfile',
  data() {
    return {
      isEditing: false
    }
  },
  computed: {
    ...mapState('user', ['user']),
    displayName() {
      return this.user?.name || 'Guest'
    }
  },
  methods: {
    ...mapActions('user', ['fetchUser']),
    toggleEdit() {
      this.isEditing = !this.isEditing
    }
  },
  mounted() {
    this.fetchUser()
  }
}
</script>

<!-- After: Composition API with script setup -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// Reactive state
const isEditing = ref(false)

// Computed
const displayName = computed(() => userStore.user?.name || 'Guest')

// Methods
function toggleEdit() {
  isEditing.value = !isEditing.value
}

// Lifecycle
onMounted(() => {
  userStore.fetchUser()
})
</script>
```

### 6. Mixin → Composable Conversion

```typescript
// Before: Mixin
// src/mixins/pagination.js
export default {
  data() {
    return {
      currentPage: 1,
      itemsPerPage: 10
    }
  },
  computed: {
    offset() {
      return (this.currentPage - 1) * this.itemsPerPage
    }
  },
  methods: {
    nextPage() {
      this.currentPage++
    },
    prevPage() {
      if (this.currentPage > 1) this.currentPage--
    }
  }
}

// After: Composable
// src/composables/usePagination.ts
import { ref, computed } from 'vue'

export function usePagination(initialPage = 1, perPage = 10) {
  const currentPage = ref(initialPage)
  const itemsPerPage = ref(perPage)

  const offset = computed(() =>
    (currentPage.value - 1) * itemsPerPage.value
  )

  function nextPage() {
    currentPage.value++
  }

  function prevPage() {
    if (currentPage.value > 1) currentPage.value--
  }

  function goToPage(page: number) {
    currentPage.value = Math.max(1, page)
  }

  return {
    currentPage,
    itemsPerPage,
    offset,
    nextPage,
    prevPage,
    goToPage
  }
}
```

### 6.1 Vue Class Component → Composition API Migration

#### Basic Class Component Migration
```vue
<!-- Before: Vue Class Component -->
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component
export default class HelloWorld extends Vue {
  // Class property = data
  message: string = 'Hello'
  count: number = 0

  // Getter = computed
  get exclamation(): string {
    return this.message + '!'
  }

  // Method
  increment(): void {
    this.count++
  }

  // Lifecycle hook
  mounted(): void {
    console.log('Component mounted')
  }
}
</script>

<!-- After: Composition API with script setup -->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// Reactive state (was class properties)
const message = ref('Hello')
const count = ref(0)

// Computed (was getter)
const exclamation = computed(() => message.value + '!')

// Method
function increment(): void {
  count.value++
}

// Lifecycle hook
onMounted(() => {
  console.log('Component mounted')
})
</script>
```

#### @Prop Decorator Migration
```vue
<!-- Before: @Prop decorator -->
<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class UserCard extends Vue {
  @Prop({ required: true }) readonly userId!: number
  @Prop({ default: 'Guest' }) readonly name!: string
  @Prop({ type: Array, default: () => [] }) readonly roles!: string[]
  @Prop({ validator: (v: number) => v > 0 }) readonly age!: number
}
</script>

<!-- After: defineProps -->
<script setup lang="ts">
interface Props {
  userId: number
  name?: string
  roles?: string[]
  age?: number
}

const props = withDefaults(defineProps<Props>(), {
  name: 'Guest',
  roles: () => []
})

// Note: validators move to runtime validation or are handled differently
// For age validation, consider using a watcher or validation library
</script>
```

#### @Emit Decorator Migration
```vue
<!-- Before: @Emit decorator -->
<script lang="ts">
import { Component, Vue, Emit } from 'vue-property-decorator'

@Component
export default class SearchInput extends Vue {
  query: string = ''

  @Emit()
  search(): string {
    return this.query
  }

  @Emit('update:modelValue')
  updateValue(value: string): string {
    return value
  }

  @Emit()
  submitForm(data: FormData): FormData {
    // Emit name derived from method name: 'submit-form'
    return data
  }
}
</script>

<!-- After: defineEmits -->
<script setup lang="ts">
import { ref } from 'vue'

const query = ref('')

const emit = defineEmits<{
  search: [query: string]
  'update:modelValue': [value: string]
  submitForm: [data: FormData]
}>()

function search(): void {
  emit('search', query.value)
}

function updateValue(value: string): void {
  emit('update:modelValue', value)
}

function submitForm(data: FormData): void {
  emit('submitForm', data)
}
</script>
```

#### @Watch Decorator Migration
```vue
<!-- Before: @Watch decorator -->
<script lang="ts">
import { Component, Vue, Watch, Prop } from 'vue-property-decorator'

@Component
export default class UserProfile extends Vue {
  @Prop() userId!: number
  userData: User | null = null

  @Watch('userId', { immediate: true, deep: false })
  onUserIdChanged(newVal: number, oldVal: number): void {
    this.fetchUser(newVal)
  }

  @Watch('userData', { deep: true })
  onUserDataChanged(newVal: User): void {
    console.log('User data changed:', newVal)
  }

  async fetchUser(id: number): Promise<void> {
    this.userData = await api.getUser(id)
  }
}
</script>

<!-- After: watch() -->
<script setup lang="ts">
import { ref, watch } from 'vue'

const props = defineProps<{
  userId: number
}>()

const userData = ref<User | null>(null)

// Watch with immediate
watch(
  () => props.userId,
  async (newVal, oldVal) => {
    await fetchUser(newVal)
  },
  { immediate: true }
)

// Deep watch
watch(
  userData,
  (newVal) => {
    console.log('User data changed:', newVal)
  },
  { deep: true }
)

async function fetchUser(id: number): Promise<void> {
  userData.value = await api.getUser(id)
}
</script>
```

#### @Ref Decorator Migration
```vue
<!-- Before: @Ref decorator -->
<script lang="ts">
import { Component, Vue, Ref } from 'vue-property-decorator'

@Component
export default class FormComponent extends Vue {
  @Ref('inputField') readonly inputRef!: HTMLInputElement
  @Ref('formElement') readonly formRef!: HTMLFormElement

  focusInput(): void {
    this.inputRef.focus()
  }

  resetForm(): void {
    this.formRef.reset()
  }
}
</script>

<template>
  <form ref="formElement">
    <input ref="inputField" type="text" />
  </form>
</template>

<!-- After: useTemplateRef (Vue 3.5+) or ref -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'

// Vue 3.5+ with useTemplateRef
const inputRef = useTemplateRef<HTMLInputElement>('inputField')
const formRef = useTemplateRef<HTMLFormElement>('formElement')

// Alternative for Vue 3.0-3.4: use ref with same name as template ref
// const inputField = ref<HTMLInputElement | null>(null)
// const formElement = ref<HTMLFormElement | null>(null)

function focusInput(): void {
  inputRef.value?.focus()
}

function resetForm(): void {
  formRef.value?.reset()
}
</script>

<template>
  <form ref="formElement">
    <input ref="inputField" type="text" />
  </form>
</template>
```

#### @PropSync and @Model Migration
```vue
<!-- Before: @PropSync and @Model -->
<script lang="ts">
import { Component, Vue, PropSync, Model } from 'vue-property-decorator'

@Component
export default class ToggleSwitch extends Vue {
  // Two-way binding with .sync modifier
  @PropSync('checked', { type: Boolean }) syncedChecked!: boolean

  // Custom v-model
  @Model('change', { type: String }) readonly value!: string
}
</script>

<!-- After: defineModel (Vue 3.4+) -->
<script setup lang="ts">
// For @PropSync - use defineModel
const checked = defineModel<boolean>('checked', { default: false })

// For @Model - use defineModel (default model)
const modelValue = defineModel<string>({ default: '' })

// Usage: checked.value = true (automatically emits update:checked)
// Usage: modelValue.value = 'new' (automatically emits update:modelValue)
</script>
```

#### @Provide/@Inject Migration
```vue
<!-- Before: @Provide/@Inject -->
<script lang="ts">
import { Component, Vue, Provide, Inject } from 'vue-property-decorator'

// Parent component
@Component
export default class ParentComponent extends Vue {
  @Provide() theme: string = 'dark'
  @Provide('apiService') api: ApiService = new ApiService()
}

// Child component
@Component
export default class ChildComponent extends Vue {
  @Inject() readonly theme!: string
  @Inject('apiService') readonly api!: ApiService
  @Inject({ from: 'optional', default: 'fallback' }) readonly optionalValue!: string
}
</script>

<!-- After: provide/inject -->
<script setup lang="ts">
import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

// Define typed injection keys (recommended)
const themeKey: InjectionKey<string> = Symbol('theme')
const apiKey: InjectionKey<ApiService> = Symbol('apiService')

// Parent component
const theme = ref('dark')
const api = new ApiService()

provide(themeKey, theme)
provide(apiKey, api)

// Child component
const theme = inject(themeKey, 'light') // with default
const api = inject(apiKey)! // assert non-null if required
const optionalValue = inject('optional', 'fallback')
</script>
```

#### vuex-class → Pinia Migration
```vue
<!-- Before: vuex-class decorators -->
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { State, Getter, Mutation, Action, namespace } from 'vuex-class'

const userModule = namespace('user')

@Component
export default class UserDashboard extends Vue {
  // Root state/getters
  @State('isLoading') isLoading!: boolean
  @Getter('isAuthenticated') isAuth!: boolean

  // Namespaced module
  @userModule.State('currentUser') user!: User
  @userModule.Getter('fullName') fullName!: string
  @userModule.Mutation('SET_USER') setUser!: (user: User) => void
  @userModule.Action('fetchUser') fetchUser!: () => Promise<void>

  mounted(): void {
    this.fetchUser()
  }
}
</script>

<!-- After: Pinia stores -->
<script setup lang="ts">
import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useAppStore } from '@/stores/app'
import { useUserStore } from '@/stores/user'

// Root store
const appStore = useAppStore()
const { isLoading } = storeToRefs(appStore)

// User store (was namespaced module)
const userStore = useUserStore()
const { currentUser: user, fullName, isAuthenticated: isAuth } = storeToRefs(userStore)

// Actions are called directly
onMounted(async () => {
  await userStore.fetchUser()
})

// Mutations are now just methods on the store
function updateUser(newUser: User): void {
  userStore.setUser(newUser) // or userStore.user = newUser
}
</script>
```

#### Class Component with Mixins Migration
```vue
<!-- Before: Class with mixins -->
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator'
import { PaginationMixin } from '@/mixins/pagination'
import { LoadingMixin } from '@/mixins/loading'

@Component
export default class UserList extends Mixins(PaginationMixin, LoadingMixin) {
  users: User[] = []

  async fetchUsers(): Promise<void> {
    this.setLoading(true)  // from LoadingMixin
    try {
      this.users = await api.getUsers(this.currentPage, this.itemsPerPage)  // from PaginationMixin
    } finally {
      this.setLoading(false)
    }
  }
}
</script>

<!-- After: Composables -->
<script setup lang="ts">
import { ref } from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useLoading } from '@/composables/useLoading'

const users = ref<User[]>([])

// Use composables instead of mixins
const { currentPage, itemsPerPage, nextPage, prevPage } = usePagination()
const { isLoading, setLoading } = useLoading()

async function fetchUsers(): Promise<void> {
  setLoading(true)
  try {
    users.value = await api.getUsers(currentPage.value, itemsPerPage.value)
  } finally {
    setLoading(false)
  }
}
</script>
```

### 7. Filter → Method/Computed Conversion

```vue
<!-- Before: Using Filter -->
<template>
  <span>{{ date | formatDate }}</span>
  <span>{{ price | currency }}</span>
</template>

<script>
export default {
  filters: {
    formatDate(value) {
      return new Date(value).toLocaleDateString()
    },
    currency(value) {
      return `$${value.toFixed(2)}`
    }
  }
}
</script>

<!-- After: Using Methods/Computed -->
<template>
  <span>{{ formatDate(date) }}</span>
  <span>{{ formatCurrency(price) }}</span>
</template>

<script setup lang="ts">
// Option 1: Local functions
function formatDate(value: string | Date): string {
  return new Date(value).toLocaleDateString()
}

function formatCurrency(value: number): string {
  return `$${value.toFixed(2)}`
}

// Option 2: Import from utility file
// import { formatDate, formatCurrency } from '@/utils/formatters'
</script>
```

### 8. Event Bus Replacement

```typescript
// Before: Event Bus
// src/eventBus.js
import Vue from 'vue'
export const EventBus = new Vue()

// Usage
EventBus.$emit('user-updated', user)
EventBus.$on('user-updated', handler)

// After: Using mitt or provide/inject
// src/utils/eventBus.ts
import mitt from 'mitt'

type Events = {
  'user-updated': User
  'notification': { message: string; type: string }
}

export const emitter = mitt<Events>()

// Usage
emitter.emit('user-updated', user)
emitter.on('user-updated', handler)
```

### 9. Global API Migration

#### Entry Point (new Vue → createApp)
```typescript
// Before: Vue 2
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import i18n from './i18n'
import MyPlugin from './plugins/myPlugin'
import GlobalComponent from './components/GlobalComponent.vue'

Vue.config.productionTip = false
Vue.use(MyPlugin, { option: true })
Vue.component('GlobalComponent', GlobalComponent)
Vue.directive('focus', { inserted(el) { el.focus() } })
Vue.mixin({ created() { console.log('global mixin') } })
Vue.prototype.$api = apiService
Vue.filter('capitalize', (val) => val.toUpperCase())

new Vue({
  router,
  store,
  i18n,
  render: h => h(App)
}).$mount('#app')

// After: Vue 3
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import i18n from './i18n'
import MyPlugin from './plugins/myPlugin'
import GlobalComponent from './components/GlobalComponent.vue'

const app = createApp(App)

app.use(createPinia())
app.use(router)
app.use(i18n)
app.use(MyPlugin, { option: true })
app.component('GlobalComponent', GlobalComponent)
app.directive('focus', { mounted(el) { el.focus() } })
app.mixin({ created() { console.log('global mixin') } })
app.config.globalProperties.$api = apiService
// Filters removed — use functions instead

app.mount('#app')
```

#### Vue.prototype → globalProperties
```typescript
// Before: Vue 2
Vue.prototype.$http = axios
Vue.prototype.$bus = new Vue()
Vue.prototype.$constants = { API_URL: '...' }

// Component usage
this.$http.get('/api')
this.$bus.$emit('event')

// After: Vue 3
app.config.globalProperties.$http = axios
app.config.globalProperties.$constants = { API_URL: '...' }
// For event bus, use mitt instead of Vue instance

// Component usage (Composition API)
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()!
proxy!.$http.get('/api')

// Better: use provide/inject or direct imports
// provide at app level:
app.provide('http', axios)
// inject in component:
const http = inject('http')
```

#### Vue.set / Vue.delete Removal
```typescript
// Before: Vue 2
Vue.set(this.obj, 'newProp', value)
this.$set(this.arr, index, value)
Vue.delete(this.obj, 'prop')
this.$delete(this.arr, index)

// After: Vue 3 (reactive by default)
this.obj.newProp = value       // or obj.value.newProp = value
this.arr[index] = value        // or arr.value[index] = value
delete this.obj.prop           // or delete obj.value.prop
this.arr.splice(index, 1)     // or arr.value.splice(index, 1)
```

#### Vue.observable → reactive
```typescript
// Before: Vue 2
const state = Vue.observable({ count: 0 })

// After: Vue 3
import { reactive } from 'vue'
const state = reactive({ count: 0 })
```

### 10. Template Syntax Migration

#### v-model Changes
```vue
<!-- Before: Vue 2 (value/input) -->
<CustomInput v-model="searchText" />
<!-- Equivalent to: <CustomInput :value="searchText" @input="searchText = $event" /> -->

<!-- After: Vue 3 (modelValue/update:modelValue) -->
<CustomInput v-model="searchText" />
<!-- Equivalent to: <CustomInput :modelValue="searchText" @update:modelValue="searchText = $event" /> -->

<!-- Component definition must change: -->
<!-- Before: props: ['value'], emit: this.$emit('input', val) -->
<!-- After: props: ['modelValue'], emit: emit('update:modelValue', val) -->

<!-- Vue 3: Multiple v-models -->
<UserForm v-model:first-name="firstName" v-model:last-name="lastName" />
```

#### .sync Modifier → v-model:prop
```vue
<!-- Before: Vue 2 (.sync) -->
<MyDialog :visible.sync="showDialog" />
<!-- Equivalent to: <MyDialog :visible="showDialog" @update:visible="showDialog = $event" /> -->

<!-- After: Vue 3 (v-model:prop) -->
<MyDialog v-model:visible="showDialog" />
```

#### .native Modifier Removal
```vue
<!-- Before: Vue 2 -->
<MyComponent @click.native="handleClick" />

<!-- After: Vue 3 -->
<!-- Option 1: Component declares 'click' in emits → treated as component event -->
<!-- Option 2: Component does NOT declare 'click' in emits → falls through to root element -->
<MyComponent @click="handleClick" />

<!-- In the child component, if click is NOT a custom event, add inheritAttrs: -->
<script setup>
// If click IS a custom event:
const emit = defineEmits(['click'])
// If click should pass through to DOM, don't declare it in emits
</script>
```

#### CSS Deep Selectors
```vue
<style scoped>
/* Before: Vue 2 */
.parent ::v-deep .child { color: red; }
.parent >>> .child { color: red; }
.parent /deep/ .child { color: red; }

/* After: Vue 3 */
.parent :deep(.child) { color: red; }

/* New in Vue 3 */
:slotted(.child) { color: red; }
:global(.app-wide) { color: red; }
</style>
```

#### v-if / v-for Precedence Change
```vue
<!-- Before: Vue 2 (v-for has higher precedence) -->
<!-- This works: v-for runs first, then v-if filters -->
<li v-for="item in items" v-if="item.active">{{ item.name }}</li>

<!-- After: Vue 3 (v-if has higher precedence) -->
<!-- This BREAKS: v-if runs first, 'item' is not yet defined -->
<!-- Fix: wrap in <template v-for> -->
<template v-for="item in items" :key="item.id">
  <li v-if="item.active">{{ item.name }}</li>
</template>

<!-- Or use computed property -->
<li v-for="item in activeItems" :key="item.id">{{ item.name }}</li>
```

#### key on <template v-for>
```vue
<!-- Before: Vue 2 (key on child) -->
<template v-for="item in items">
  <div :key="item.id">{{ item.name }}</div>
</template>

<!-- After: Vue 3 (key on template) -->
<template v-for="item in items" :key="item.id">
  <div>{{ item.name }}</div>
</template>
```

#### $listeners Removal
```vue
<!-- Before: Vue 2 -->
<template>
  <ChildComponent v-bind="$attrs" v-on="$listeners" />
</template>

<!-- After: Vue 3 ($listeners merged into $attrs) -->
<template>
  <ChildComponent v-bind="$attrs" />
</template>
```

#### $scopedSlots → $slots
```vue
<!-- Before: Vue 2 -->
<script>
export default {
  render(h) {
    return h('div', [
      this.$scopedSlots.default({ item: this.item }),
      this.$scopedSlots.header && this.$scopedSlots.header({ title: this.title })
    ])
  }
}
</script>

<!-- After: Vue 3 (all slots are functions) -->
<script>
import { h } from 'vue'
export default {
  setup(props, { slots }) {
    return () => h('div', [
      slots.default?.({ item: props.item }),
      slots.header?.({ title: props.title })
    ])
  }
}
</script>
```

#### emits Declaration
```vue
<!-- Vue 3 requires declaring emitted events -->
<script setup>
// All custom events must be declared
const emit = defineEmits<{
  change: [value: string]
  'update:modelValue': [value: string]
  submit: []
}>()

// Or with validation
const emit = defineEmits({
  change: (value: string) => typeof value === 'string',
  submit: null // no validation
})
</script>
```

### 11. Custom Directives Migration

```typescript
// Before: Vue 2 directive hooks
const myDirective = {
  bind(el, binding, vnode) { /* on first bind */ },
  inserted(el, binding, vnode) { /* after element inserted into DOM */ },
  update(el, binding, vnode, oldVnode) { /* on VNode update */ },
  componentUpdated(el, binding, vnode, oldVnode) { /* after children update */ },
  unbind(el, binding, vnode) { /* on unbind */ }
}

// After: Vue 3 directive hooks (aligned with component lifecycle)
const myDirective = {
  created(el, binding, vnode) { /* new: before element attributes applied */ },
  beforeMount(el, binding, vnode) { /* was: bind */ },
  mounted(el, binding, vnode) { /* was: inserted */ },
  beforeUpdate(el, binding, vnode, prevVnode) { /* new */ },
  updated(el, binding, vnode, prevVnode) { /* was: componentUpdated */ },
  beforeUnmount(el, binding, vnode) { /* new */ },
  unmounted(el, binding, vnode) { /* was: unbind */ }
}

// Hook name mapping:
// bind → beforeMount
// inserted → mounted
// update → (removed, split into beforeUpdate + updated)
// componentUpdated → updated
// unbind → unmounted

// Note: vnode.context (component instance) is removed in Vue 3
// Use binding.instance instead
```

### 12. Environment Variables & Asset Handling (Vue CLI → Vite)

#### Environment Variables
```typescript
// Before: Vue CLI (process.env.VUE_APP_*)
const apiUrl = process.env.VUE_APP_API_URL
const nodeEnv = process.env.NODE_ENV
const baseUrl = process.env.BASE_URL

// After: Vite (import.meta.env.VITE_*)
const apiUrl = import.meta.env.VITE_API_URL
const mode = import.meta.env.MODE         // 'development' | 'production'
const baseUrl = import.meta.env.BASE_URL
const isProd = import.meta.env.PROD        // boolean
const isDev = import.meta.env.DEV          // boolean

// .env files: rename VUE_APP_ prefix to VITE_
// .env.development: VUE_APP_API_URL=... → VITE_API_URL=...
// .env.production: VUE_APP_API_URL=... → VITE_API_URL=...
```

#### Static Asset Imports
```typescript
// Before: Webpack (require)
const img = require('@/assets/logo.png')
const component = require('./MyComponent.vue').default

// After: Vite (import / new URL)
import img from '@/assets/logo.png'
import component from './MyComponent.vue'

// Dynamic images
// Before: require(`@/assets/${name}.png`)
// After: new URL(`../assets/${name}.png`, import.meta.url).href
```

#### require.context → import.meta.glob
```typescript
// Before: Webpack require.context
const requireComponent = require.context('./components', true, /\.vue$/)
requireComponent.keys().forEach(fileName => {
  const componentConfig = requireComponent(fileName)
  const componentName = fileName.replace(/^\.\//, '').replace(/\.vue$/, '')
  Vue.component(componentName, componentConfig.default || componentConfig)
})

// After: Vite import.meta.glob
const modules = import.meta.glob('./components/**/*.vue', { eager: true })
for (const path in modules) {
  const componentName = path.replace(/^\.\/components\//, '').replace(/\.vue$/, '')
  app.component(componentName, (modules[path] as any).default)
}

// Lazy loading variant
const lazyModules = import.meta.glob('./components/**/*.vue')
// Returns: { './components/Foo.vue': () => import('./components/Foo.vue') }
```

#### Global SCSS Variables (vue.config.js → vite.config.ts)
```typescript
// Before: vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
}

// After: vite.config.ts
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
})
```

#### Proxy Configuration
```typescript
// Before: vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  }
}

// After: vite.config.ts
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  }
})
```

### 13. Render Function & JSX Migration

```typescript
// Before: Vue 2 render function (h passed as argument)
export default {
  render(h) {
    return h('div', {
      attrs: { id: 'app' },
      class: { active: this.isActive },
      style: { color: 'red' },
      on: { click: this.handleClick },
      domProps: { innerHTML: this.content },
      key: 'my-key',
      ref: 'myRef'
    }, [
      h('span', 'Hello'),
      this.$slots.default
    ])
  }
}

// After: Vue 3 render function (import h, flat props)
import { h, ref } from 'vue'

export default {
  setup(props, { slots }) {
    const isActive = ref(true)

    return () => h('div', {
      id: 'app',                    // attrs are flat
      class: { active: isActive.value },
      style: { color: 'red' },
      onClick: handleClick,          // events use onXxx
      innerHTML: content,            // domProps are flat
      key: 'my-key',
      ref: myRef
    }, [
      h('span', 'Hello'),
      slots.default?.()              // slots are functions
    ])
  }
}

// Key changes:
// 1. h() is imported from 'vue', not passed as argument
// 2. Props are FLAT (no nesting under attrs/on/domProps)
// 3. Event listeners use onXxx format (on: { click } → onClick)
// 4. $slots.default → slots.default?.()
// 5. $scopedSlots.name(props) → slots.name?.(props)

// JSX changes:
// Before: Vue 2 JSX (with babel-plugin-transform-vue-jsx)
// After: Vue 3 JSX (with @vue/babel-plugin-jsx)
// Most JSX syntax stays the same, but:
// - vModel → v-model (same)
// - v-on:click → onClick (already JSX convention)
// - Slots: this.$slots → slots from setup context
```

### 14. Async Components & Functional Components

#### Async Components
```typescript
// Before: Vue 2
const AsyncComp = () => import('./MyComponent.vue')
const AsyncCompWithOptions = () => ({
  component: import('./MyComponent.vue'),
  loading: LoadingComponent,
  error: ErrorComponent,
  delay: 200,
  timeout: 3000
})

// After: Vue 3
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => import('./MyComponent.vue'))
const AsyncCompWithOptions = defineAsyncComponent({
  loader: () => import('./MyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})
```

#### Functional Components
```vue
<!-- Before: Vue 2 functional component (SFC) -->
<template functional>
  <div>
    <p>{{ props.title }}</p>
    <slot></slot>
  </div>
</template>

<!-- After: Vue 3 (just a regular component, or plain function) -->
<!-- Option 1: Regular SFC (Vue 3 already optimizes stateless components) -->
<template>
  <div>
    <p>{{ title }}</p>
    <slot></slot>
  </div>
</template>
<script setup>
defineProps(['title'])
</script>

<!-- Option 2: Render function -->
<script>
import { h } from 'vue'
export default function MyFunctionalComp(props, { slots }) {
  return h('div', [
    h('p', props.title),
    slots.default?.()
  ])
}
MyFunctionalComp.props = ['title']
</script>
```

```typescript
// Before: Vue 2 functional component (JS)
Vue.component('my-comp', {
  functional: true,
  props: ['msg'],
  render(h, context) {
    return h('div', context.props.msg)
    // context.children, context.slots(), context.data, context.parent
  }
})

// After: Vue 3
import { h } from 'vue'
function MyComp(props) {
  return h('div', props.msg)
}
MyComp.props = ['msg']
```

### 15. Transition Class Name Changes

```css
/* Before: Vue 2 */
.fade-enter { opacity: 0; }
.fade-enter-active { transition: opacity 0.3s; }
.fade-enter-to { opacity: 1; }
.fade-leave { opacity: 1; }
.fade-leave-active { transition: opacity 0.3s; }
.fade-leave-to { opacity: 0; }

/* After: Vue 3 (enter/leave renamed to enter-from/leave-from) */
.fade-enter-from { opacity: 0; }     /* was: .fade-enter */
.fade-enter-active { transition: opacity 0.3s; }
.fade-enter-to { opacity: 1; }
.fade-leave-from { opacity: 1; }     /* was: .fade-leave */
.fade-leave-active { transition: opacity 0.3s; }
.fade-leave-to { opacity: 0; }

/* Also: <transition-group> no longer renders a wrapper <span> by default */
/* Add tag="div" or tag="ul" if you need a wrapper element */
```

### 16. Testing Migration (@vue/test-utils v1 → v2)

```typescript
// Before: @vue/test-utils v1 (Vue 2)
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'

const localVue = createLocalVue()
localVue.use(Vuex)

const store = new Vuex.Store({ state: { count: 0 } })
const wrapper = shallowMount(MyComponent, {
  localVue,
  store,
  mocks: { $route: { params: { id: 1 } } },
  stubs: ['router-link'],
  propsData: { title: 'Hello' }
})

expect(wrapper.find('.title').text()).toBe('Hello')

// After: @vue/test-utils v2 (Vue 3)
import { shallowMount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'

const pinia = createPinia()
setActivePinia(pinia)

const wrapper = shallowMount(MyComponent, {
  global: {
    plugins: [pinia],
    mocks: { $route: { params: { id: 1 } } },
    stubs: ['RouterLink']  // PascalCase now
  },
  props: { title: 'Hello' }  // was: propsData
})

expect(wrapper.find('.title').text()).toBe('Hello')

// Key changes:
// - createLocalVue() removed (use global.plugins)
// - propsData → props
// - mocks/stubs/provide moved under global
// - localVue.use() → global.plugins: []
// - attachToDocument → attachTo
// - wrapper.destroy() → wrapper.unmount()
// - Many methods now async (trigger, setValue, etc.)
```

### 17. Third-Party Library Migration Patterns

#### vue-i18n v8 → v9
```typescript
// Before: vue-i18n v8
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
const i18n = new VueI18n({
  locale: 'en',
  messages: { en: { hello: 'Hello' } }
})
new Vue({ i18n }).$mount('#app')
// Usage: this.$t('hello'), {{ $t('hello') }}

// After: vue-i18n v9
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
  legacy: false, // use Composition API mode
  locale: 'en',
  messages: { en: { hello: 'Hello' } }
})
app.use(i18n)
// Usage in <script setup>:
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
// Usage in template: {{ t('hello') }} or {{ $t('hello') }}
```

#### portal-vue → Teleport
```vue
<!-- Before: portal-vue -->
<template>
  <portal to="modal">
    <div class="modal">Content</div>
  </portal>
</template>

<!-- After: Built-in Teleport -->
<template>
  <Teleport to="#modal">
    <div class="modal">Content</div>
  </Teleport>
</template>
<!-- Note: target must be a CSS selector, not a portal name -->
<!-- Add <div id="modal"></div> to index.html -->
```

#### vuex-persistedstate → pinia-plugin-persistedstate
```typescript
// Before: vuex-persistedstate
import createPersistedState from 'vuex-persistedstate'
const store = new Vuex.Store({
  plugins: [createPersistedState({ paths: ['user'] })]
})

// After: pinia-plugin-persistedstate
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

// In store definition:
export const useUserStore = defineStore('user', () => {
  // ...state and actions
}, {
  persist: true // or { paths: ['user'] }
})
```

#### VeeValidate v3 → v4
```vue
<!-- Before: VeeValidate v3 -->
<template>
  <ValidationObserver v-slot="{ handleSubmit }">
    <form @submit.prevent="handleSubmit(onSubmit)">
      <ValidationProvider rules="required|email" v-slot="{ errors }">
        <input v-model="email" />
        <span>{{ errors[0] }}</span>
      </ValidationProvider>
    </form>
  </ValidationObserver>
</template>

<!-- After: VeeValidate v4 (Composition API) -->
<template>
  <form @submit="onSubmit">
    <input v-model="email" />
    <span>{{ emailError }}</span>
  </form>
</template>
<script setup>
import { useForm, useField } from 'vee-validate'
import * as yup from 'yup'

const { handleSubmit } = useForm({
  validationSchema: yup.object({ email: yup.string().required().email() })
})

const { value: email, errorMessage: emailError } = useField('email')

const onSubmit = handleSubmit((values) => {
  console.log(values)
})
</script>
```

## Implementation Checklist

### Phase 1: Core Setup
- [ ] Update package.json dependencies
- [ ] Configure Vite (if migrating from Vue CLI)
- [ ] Update main entry point (`new Vue()` → `createApp()`)
- [ ] Migrate all `Vue.use()` → `app.use()`
- [ ] Migrate all `Vue.component()` → `app.component()`
- [ ] Migrate all `Vue.directive()` → `app.directive()` (with renamed hooks)
- [ ] Migrate all `Vue.mixin()` → `app.mixin()` (or convert to composables)
- [ ] Migrate `Vue.prototype.$x` → `app.config.globalProperties.$x`
- [ ] Remove `Vue.config.productionTip` and other removed configs
- [ ] Configure TypeScript (tsconfig.json)
- [ ] Update type declaration files (shims-vue.d.ts)

### Phase 2: Environment & Build Migration (if Vue CLI → Vite)
- [ ] Rename env variables: `VUE_APP_*` → `VITE_*` in .env files
- [ ] Replace `process.env.VUE_APP_*` → `import.meta.env.VITE_*` in code
- [ ] Replace `process.env.NODE_ENV` → `import.meta.env.MODE`
- [ ] Replace `require()` → `import` statements
- [ ] Replace `require.context()` → `import.meta.glob()`
- [ ] Migrate `vue.config.js` proxy → `vite.config.ts` proxy
- [ ] Migrate global SCSS/LESS variable injection
- [ ] Move `index.html` to project root (Vite requirement)
- [ ] Update asset references (remove `~` prefix in SCSS)

### Phase 3: Router Migration
- [ ] Convert router configuration (`mode: 'history'` → `createWebHistory()`)
- [ ] Update navigation guards (signature changes)
- [ ] Fix route meta typing
- [ ] Update component route usage

### Phase 4: State Management
- [ ] Create Pinia stores from Vuex modules
- [ ] Migrate state, getters, actions (remove mutations)
- [ ] Replace `mapState`/`mapGetters`/`mapActions` with `storeToRefs`
- [ ] Update component store usage
- [ ] Migrate `vuex-persistedstate` → `pinia-plugin-persistedstate`
- [ ] Remove Vuex completely

### Phase 5: Component Migration
- [ ] Convert Options API to script setup syntax
- [ ] Convert Class Components to Composition API
- [ ] 

…(truncated)
