Migrating Vue 2 to Vue 3 With AI

3 min read

I’ve got a legacy codebase that is about 500k lines of code. Half is Typescript and the other half is Vue 2 single-file components. Bundlers, IDEs, and plugins all show waning support for Vue 2. I can’t even get WebStorm’s Typescript service to handle .vue files anymore. Clearly it’s time to migrate to Vue 3, but 800 .vue files makes this a daunting prospect.

Ends up, AI (Anthropic’s Claude 3.5 Sonnet) is just barely capable of doing this migration for me.

Prepping The Codebase

My codebase was using Vue 2.6.14 with @vue/composition-api. My goal was to update to Vue 3.5.12 which is the current latest.

First off, I needed to address a few libraries (like Vuetify) that were built specifically for Vue 2. I actually don’t care for Vuetify (maybe v3 is better?) so I removed it all-together which took about a week.

Ends up, I actually prefer not to have any Vue specific libraries in my app. For example, many projects like sortable-foo might have a vue specific version vue-sortable-foo. Those are almost always light-weight wrappers. So I prefer to use the base library with my own wrappers. Then I can stay out of Vue’s private APIs and migration should be much easier if a Vue 4 comes out.

Finally, I deployed this new codebase which is still Vue 2 but has no Vue 2 specific dependencies.

Migrating Vue 2 Single File Components

Next up, I updated my bundler (webpack) and my package.json to Vue 3. The fun begins.

First off, I built a migration script to find all “*.vue” files in my project and process them one-at-a-time. I use AI to migrate the file to Vue 3, then save the original as “-ORIG.vue”, then save the new code over the original file. This means, if there is an “ORIG” version then the real file has been migrated. It also allows me to easily diff the two files. Later on, I’ll delete all the ORIG files.

Next I needed to pick an LLM. OpenAI’s ChatGPT 4o has been so bad for me lately that I cancelled my account. After testing several options, Anthropic’s Claude 3.5 Sonnet 2024-10-22 was the easy winner.

The Vue 2->3 Migration Prompt

For the most part, just telling Claude to “migrate my code from Vue 2 to Vue 3” was fine for simple components but a few things needed tweaking:

  • it added or modified Typescript types unnecessarily
  • it removed comments
  • it exported things that weren’t previously exported (or forgot to export something)
  • I didn’t like the types it chose for defineProps
  • It fell apart with calls to $children, vnodes, etc
  • It couldn’t handle $listeners being gone
  • The key property moved for some v-for calls

These changes above were easy to fix with a few words in the prompt. However there were two categories that were a major challenge… reactive(...) and exports within a .vue file.

Dealing with reactive(…)

With @vue/composition-api, reactive(...) modifies the target object. In Vue 3 it returns a new (proxied) object. Moreover, in Vue 2, you needed to wait until all properties were initialized before you called reactive – new properties would not become reactive unless you use Vue.set. In Vue 3, you really need reactive(...) to be called at the initial declaration – or else you have two versions floating around. As you might imagine, this complexity is at the limit for current language models.

Exports within a .vue file

Another guilty habit of my codebase was to export Interfaces and functions from the .vue file itself. For example:

<script lang="ts">
export interface ILaunchDropdownModalArgs {
  title: string;
}
const Modal = defineComponent({
  setup(props){
    return { dropdownParams: { ... } };
  },
});
export default Modal;
export const launchDropdownModal = async (args:ILaunchDropdownModalArgs) => {
  return launchModal({ component: Modal, args });
};
</script>

This is a pretty common pattern in my SAAS and frankly I like it. However, in Vue 3, we cannot export anything within the “setup” script which means we need to add another script tag for the exports. Getting the correct code within the correct script tag is tough. The LLM often forgets to export something. Or it does move an export to the new script but forgets to import a dependency.

<script lang="ts">
import Modal from './dropdown-modal.vue';
export interface ILaunchDropdownModalArgs {
  title: string;
}
export const launchDropdownModal = async (args:ILaunchDropdownModalArgs) => {
  return launchModal({ component: Modal, args });
};
</script>
<script lang="ts" setup>
const dropdownParams = { ... };
</script>

The Vue2 to Vue 3 AI Migration Prompts

Ultimately the LLM was unable to deal with reactive and exports at the same time, so I used two passes through AI. The first pass did the general migration to Vue 3 but was told to keep exports within the setup script. The second pass focused 100% on splitting the script tags.

I also removed the style tags during processing because (1) LLMs aren’t always great at repeating something verbatim and (2) it saved the token cost for this input/output.

  • Remove the style tag
  • LLM converts the Vue 2 code to Vue 3 (see the prompt)
  • LLM splits exports into a separate script tag (see the prompt)
  • Style tag is re-applied

I’ve linked the prompts above. You’ll see I also included a few project-specific instructions – like objects that should always be created as “reactive”.

I also instructed the LLM to add a “TODO” comment for any code it was unsure about. Of 800 “.vue” files it included 85 “todos” – mainly regarding “reactive” objects from a different scope.

Typescript Files

I did a separate AI-based migration for about 200 *.ts files that referenced Vue in some way. The main considerations were:

  • reactive(...) (ugh… once again)
  • Directive hooks have been renamed
  • Vue.set and Vue.delete no longer exist
  • Internals such as $children have changed a bit

Here is the prompt I ended up using for Typescript files.

Vue 3 also changed the way you mount the root component, so I fixed that manually.

Cleanup

It took me two days to build the migration and refine these prompts. Then I ran the migration overnight. Then several hours reviewing the “TODO” concerns from the AI. The biggest AI “todo concerns” were:

  • reactivity from other scopes
  • the AI was uncomfortable with under-defined types (such as any)
  • the AI wanted me to double check all jQuery references (lol, unrelated to this migration)

Then I began trying to run the application. These are the main issues I found:

  • Despite my best efforts, AI forgot to export quite a few previously exported types
  • AI created several new types and exported them
  • In a few cases, an extremely simple .vue file was turned into a massive file full of fake code ;)
  • Several .vue files did not end up getting the setup attribute on the script tag

Leave a Reply

Your email address will not be published. Required fields are marked *