Skip to content

Using AI for refactoring Vue 2 to Vue 3

2022 has been an incredible year for AI-generated content. I see that as a tool, the same way you listen to music and create music afterwards. The same way my developer journey has been from reading open-source code, and contributing to the field.

I have a project made with Vue 2 using Typescript, Vue Property Decorator and Quasar. My goal was to refactor to Vue 3 using Typescript with Composition API.
My time is limited on the subject and not a big priority, and the amount of work seems too much for my bare hand that, 2 years ago, that i dropped it.

We are in 2023, could AI help me for a such task ?

TDLR

Gist: https://gist.github.com/tito/230863f714ab43fc92ad55510de2ca4e

The test case

Here is my “test” source, where I’ll experiment the refactoring. The actual file is actually longer as it include template and style tag, but it doesn’t matter for our example.

Vue 2 using Vue Property Decorator and Typescript
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';

@Component
export default class Login extends Vue {
  public $store: any;
  public $q: any;
  public $route: any;
  public loading: boolean = false;
  public error: any = null;
  public email: string = '';
  public password: string = '';

  public async created() {
    const msgkey = this.$route.params.msg;
    if (msgkey === 'LOGOUT_SUCCESS') {
      this.$q.notify('Vous êtes déconnecté');
    }
    if (this.$route.query.email !== undefined) {
      this.email = this.$route.query.email;
    }
  }

  public handleSubmit() {
    this.error = null;
    if (this.password.length > 0) {
      this.loading = true;
      this.$store.dispatch('login', {
        email: this.email,
        password: this.password,
      }).catch((error: any) => {
        this.error = 'Email ou mot de passe invalide';
        this.loading = false;
      });
    }
  }
}
</script>

Copilot

I use copilot day-to-day as my advanced auto completion. While i don’t use it for creating a new file from scratch, it is a very handy tool for auto completion for repetitive tasks, from variable declarations, object copying, filter/map…

But that’s all it is. I cannot really take a whole file, and give instruction to copilot for converting from one syntax to another…

ChatGPT

Using OpenAI ChatGPT API, i got a prompt that fairly give me the output i wanted.

ChatGPT Prompt
You are an assistant design to help developper for migrating their code from Vue 2 to Vue 3 using Typescript with Composition API. Here is a set of rules you must absolutely follow:

1. Rewrite the <script lang="ts"> to <script setup lang="ts">
2. The content of the script tag must be a valid Typescript code
3. The component must be flattened into the script setup
4. Remove any "export default".
5. Use the `onMounted` hook instead of the `created` lifecycle hook if necessary
6. Use the `useRoute` approach instead of $route. Same for $router.
7. Store is not using vuex but pinia.
8. Auth related function is accessible in stores/auth.ts, using useAuthStore.
9. Do not use Ref is the type can be infered from the value pass into ref()
10. Do not put all the methods and properties into a global const object
11. Prefer using global "const router = useRouter()" instead of live instanciation when needed

My problem is, result are not consistent, and do follow my rules.

First generation

Not as perfect as wanted. It choose to not use onMounted(), and flattened the code, which is allowed by the instruction. However, this.$q is here, which end up failing.

Vue
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { ref } from 'vue';

const router = useRouter();
const authStore = useAuthStore();

const loading = ref(false);
const error = ref(null);
const email = ref('');
const password = ref('');

const handleSubmit = () => {
    error.value = null;
    if (password.value.length > 0) {
      loading.value = true;
      authStore.login({ email: email.value, password: password.value})
        .catch((error: any) => {
          error.value = 'Email ou mot de passe invalide';
          loading.value = false;
        });
    }
};

const msgkey = router.params.msg;
if (msgkey === 'LOGOUT_SUCCESS') {
  this.$q.notify('Vous êtes déconnecté');
}
if (router.query.email !== undefined) {
  email.value = router.query.email as string;
}

</script>

Second generation

That’s what bother me. By giving the same input, i have a now a different output.
It recognized Quasar, and put the code into the onMounted(). Works for me.
BUT, now the login function don’t have the same input as before, it decided to flatten the argument. OK.

Vue
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { onMounted, ref } from 'vue';
import { useQuasar } from 'quasar';
import { useAuthStore } from '@/stores/auth';

const loading = ref(false);
const error = ref(null);
const email = ref('');
const password = ref('');

const { notify } = useQuasar();
const route = useRoute();
const authStore = useAuthStore();

onMounted(() => {
  const msgkey = route.params.msg;
  if (msgkey === 'LOGOUT_SUCCESS') {
    notify('Vous êtes déconnecté');
  }
  if (route.query.email !== undefined) {
    email.value = route.query.email as string;
  }
});

const handleSubmit = () => {
  error.value = null;
  if (password.value.length > 0) {
    loading.value = true;
    authStore.login(email.value, password.value)
      .catch((error: any) => {
        error.value = 'Email ou mot de passe invalide';
        loading.value = false;
      });
  }
};
</script>

When it went wrong

I tried to play with temperature, top_n, but from time to time, it generate something against the rules.

Everything under a global Login() function.. nop
<script setup lang="ts">
...
const Login = () => {
  const store = useAuthStore();
  const router = useRouter();
  const loading = ref(false);
  const error = ref(null);
  const email = ref('');
  const password = ref('');

  onMounted(() => {
    ...
  });

  const handleSubmit = () => {
    ...
  }

  return {
    loading,
    error,
    email,
    password,
    handleSubmit
  }
}
</script>
Using export default defineComponent(), against rule #4
<script setup lang="ts">
...
export default defineComponent({
  components: {
    GaugeChart,
  },
  setup() {
    const id = ref<number>();
    const loading = ref(false);
    const mapZoom = 8;
    const resources = ref([]);
...

Conclusion

I can go again and give another set of rules to use for the migration, especially moving from a single store using Vuex to many stores using Pinia.

Out of 92 files needed to convert, totally over 22997 lines to convert, around 10 needed a refactoring by hand as i was not able to generate a correct approach.
Nevertheless, it was much faster to go other it with the tool and review rather than manually pass through everything.

For today, i take that as a win.