Minimal @vue/compat configuration
Run Vue 3 by default and keep only the Vue 2 behaviors this project still relies on. Remove each flag as you fix the matching findings.
// main.js (with 'vue' aliased to '@vue/compat')
import { configureCompat } from 'vue'
configureCompat({
MODE: 3,
COMPONENT_FUNCTIONAL: true,
COMPONENT_V_MODEL: true,
CONFIG_DEVTOOLS: true,
CONFIG_IGNORED_ELEMENTS: true,
CONFIG_KEY_CODES: true,
CONFIG_PRODUCTION_TIP: true,
CUSTOM_DIR: true,
FILTERS: true,
GLOBAL_DELETE: true,
GLOBAL_MOUNT: true,
GLOBAL_PROTOTYPE: true,
GLOBAL_SET: true,
INSTANCE_ATTRS_CLASS_STYLE: true,
INSTANCE_CHILDREN: true,
INSTANCE_DELETE: true,
INSTANCE_DESTROY: true,
INSTANCE_EVENT_EMITTER: true,
INSTANCE_EVENT_HOOKS: true,
INSTANCE_LISTENERS: true,
INSTANCE_SCOPED_SLOTS: true,
INSTANCE_SET: true,
OPTIONS_BEFORE_DESTROY: true,
OPTIONS_DATA_FN: true,
OPTIONS_DESTROYED: true,
RENDER_FUNCTION: true,
TRANSITION_CLASSES: true,
TRANSITION_GROUP_ROOT: true,
V_ON_KEYCODE_MODIFIER: true,
WATCH_ARRAY: true,
})
// vite.config.js
import vue from '@vitejs/plugin-vue'
export default {
resolve: { alias: { vue: '@vue/compat' } },
plugins: [
vue({
template: {
compilerOptions: {
compatConfig: {
MODE: 3,
COMPILER_FILTERS: true,
COMPILER_INLINE_TEMPLATE: true,
COMPILER_IS_ON_ELEMENT: true,
COMPILER_V_BIND_OBJECT_ORDER: true,
COMPILER_V_BIND_SYNC: true,
COMPILER_V_IF_V_FOR_PRECEDENCE: true,
COMPILER_V_ON_NATIVE: true,
},
},
},
}),
],
}
Findings
Errorglobal-apiGlobal Vue API moved to the app instance8
Vue.component, Vue.use, Vue.mixin and Vue.directive become app.component, app.use, app.mixin and app.directive. Vue.nextTick and Vue.observable become named imports (nextTick, reactive).
-
src/legacy/inline.js:3:1Vue.component is removed from the global API: use app.component().
Vue.component('legacy-counter', { -
src/main.js:16:1Vue.use is removed from the global API: use app.use().
Vue.use(BootstrapVue) -
src/main.js:17:1Vue.mixin is removed from the global API: use app.mixin().
Vue.mixin(tracking) -
src/main.js:18:1Vue.component is removed from the global API: use app.component().
Vue.component('BaseButton', BaseButton) -
src/main.js:19:1Vue.directive is removed from the global API: use app.directive().
Vue.directive('tooltip', tooltip) -
src/main.js:20:1Vue.directive is removed from the global API: use app.directive().
Vue.directive('focus', { -
src/router/index.js:5:1Vue.use is removed from the global API: use app.use().
Vue.use(VueRouter) -
src/store/index.js:4:1Vue.use is removed from the global API: use app.use().
Vue.use(Vuex)
ErrorfiltersFilters are removed6
Filters ({{ price | currency }}, Vue.filter, the filters option) no longer exist. Replace them with method calls, computed properties or global properties.
-
src/components/ProductList.vue:5:25Filter "capitalize" used in a template expression. Call a method or use a computed property instead.
{{ product.name | capitalize }} costs {{ product.price | currency('EUR') }} -
src/components/ProductList.vue:5:64Filter "currency" used in a template expression. Call a method or use a computed property instead.
{{ product.name | capitalize }} costs {{ product.price | currency('EUR') }} -
src/components/ProductList.vue:6:69Filter "capitalize" used in a template expression. Call a method or use a computed property instead.
<product-card :product.sync="selected" :title="product.name | capitalize" /> -
src/components/ProductList.vue:30:5Local filter "capitalize" is removed: turn it into a method or a computed property.
capitalize(value) { -
src/legacy/inline.js:7:55Filter "number" used in a template expression. Call a method or use a computed property instead.
template: '<button @click.native="count++">{{ count | number }}</button>', -
src/main.js:26:1Vue.filter is removed: expose a helper through app.config.globalProperties or import a function.
Vue.filter('currency', (value) => `$${Number(value).toFixed(2)}`)
Errorset-delete$set, $delete, Vue.set and Vue.delete are removed5
Vue 3 reactivity is based on Proxy, so plain assignment and the delete operator are tracked. Replace this.$set(obj, key, value) with obj[key] = value.
-
src/components/ProductList.vue:55:12$set is removed: assign the property directly, Vue 3 tracks it.
this.$set(product, 'inCart', true) -
src/store/index.js:10:7Vue.set is removed: assign the property directly, Vue 3 tracks it.
Vue.set(state.cart, product.id, product) -
src/store/index.js:13:7Vue.delete is removed: use the delete operator, Vue 3 tracks it.
Vue.delete(state.cart, id) -
src/views/Home.vue:23:10$set is removed: assign the property directly, Vue 3 tracks it.
this.$set(this.form, 'email', '') -
src/views/Home.vue:24:10$delete is removed: use the delete operator, Vue 3 tracks it.
this.$delete(this.form, 'legacy')
Errordirective-hooksCustom directive hooks are renamed4 auto fixable4
Directive hooks follow the component lifecycle: bind is beforeMount, inserted is mounted, componentUpdated is updated, unbind is unmounted, and update was removed.
-
src/directives/tooltip.js:2:3 auto fixDirective hook bind is renamed to beforeMount.
bind(el, binding) { -
src/directives/tooltip.js:5:3 auto fixDirective hook componentUpdated is renamed to updated.
componentUpdated(el, binding) { -
src/directives/tooltip.js:8:3 auto fixDirective hook unbind is renamed to unmounted.
unbind(el) { -
src/main.js:21:3 auto fixDirective hook inserted is renamed to mounted.
inserted(el) {
Errordestroy-hooksbeforeDestroy and destroyed are renamed3 auto fixable3
beforeDestroy is now beforeUnmount and destroyed is now unmounted. The old hooks are silently ignored.
-
src/components/Modal.vue:27:3 auto fixdestroyed is renamed to unmounted and is ignored by Vue 3.
destroyed() { -
src/components/ProductList.vue:50:3 auto fixbeforeDestroy is renamed to beforeUnmount and is ignored by Vue 3.
beforeDestroy() { -
src/mixins/tracking.js:5:3 auto fixbeforeDestroy is renamed to beforeUnmount and is ignored by Vue 3.
beforeDestroy() {
Errorkeycode-modifiersKeyCode modifiers are removed2 auto fixable3
Numeric modifiers such as @keyup.13 must use key names such as @keyup.enter.
-
src/components/TheHeader.vue:5:55 auto fixKeyCode modifier .13 is removed: use .enter.
<input v-model="query" placeholder="Search" @keyup.13="search" @keyup.27="query = ''" @keydown.112="help" /> -
src/components/TheHeader.vue:5:74 auto fixKeyCode modifier .27 is removed: use .esc.
<input v-model="query" placeholder="Search" @keyup.13="search" @keyup.27="query = ''" @keydown.112="help" /> -
src/components/TheHeader.vue:5:99KeyCode modifier .112 is removed: use the kebab-case key name instead.
<input v-model="query" placeholder="Search" @keyup.13="search" @keyup.27="query = ''" @keydown.112="help" />
Errorscoped-slots$scopedSlots is removed2 auto fixable2
All slots are exposed on $slots as functions. $scopedSlots.x becomes $slots.x.
-
src/components/FancyList.js:7:46 auto fix$scopedSlots is removed: use $slots, which now exposes slots as functions.
this.items.map((item) => h('li', [this.$scopedSlots.default({ item })])), -
src/components/Modal.vue:7:16 auto fix$scopedSlots is removed: use $slots, which now exposes slots as functions.
<div v-if="$scopedSlots.extra" class="extra">
Errorevent-emitter$on, $off and $once are removed2
Instances are no longer event emitters, so event buses built on new Vue() stop working. Use props and emits, provide/inject, a store, or a small library such as mitt.
-
src/components/ProductList.vue:47:14$on is removed: instances are no longer event emitters (use props and emits, provide/inject, a store, or mitt).
EventBus.$on('cart:add', this.onAdd) -
src/components/ProductList.vue:51:14$off is removed: instances are no longer event emitters (use props and emits, provide/inject, a store, or mitt).
EventBus.$off('cart:add', this.onAdd)
Errorglobal-mountnew Vue() is replaced by createApp()2
Create the root with createApp(App) and mount it with app.mount('#app').
-
src/event-bus.js:3:25new Vue() is removed: create the app with createApp(App) and call app.mount().
export const EventBus = new Vue() -
src/main.js:29:1new Vue() is removed: create the app with createApp(App) and call app.mount().
new Vue({
Errorcomponent-v-modelv-model on components uses modelValue2
A component used with v-model now receives modelValue and must emit update:modelValue. The value prop, the input event and the model option no longer apply.
-
src/components/BaseInput.vue:12:11Prop "value" combined with $emit('input'): v-model on components now passes modelValue and listens to update:modelValue.
props: ['value', 'label'], -
src/components/CustomSelect.vue:10:3The model option is removed: v-model now uses the modelValue prop and the update:modelValue event (or v-model:name arguments).
model: {
Errorrender-function-hRender functions no longer receive h2
Import h from vue instead of taking it as an argument. The VNode props shape is also flatter (on, attrs and domProps are gone).
-
src/components/FancyList.js:4:10render(h) no longer receives h: import { h } from 'vue' and update the VNode props format.
render(h) { -
src/main.js:32:12render(h) no longer receives h: import { h } from 'vue' and update the VNode props format.
render: (h) => h(App),
Errordeprecated-slot-syntaxslot and slot-scope attributes are removed2
The slot, slot-scope and scope attributes were deprecated in Vue 2.6 and removed in Vue 3. Use v-slot (or #name).
-
src/components/LegacyWidget.vue:11:17The slot attribute is removed: use v-slot (or #name) on a <template>.
<template slot="footer"> -
src/components/ProductList.vue:16:17slot-scope is removed: use v-slot="props" (or #name="props").
<template slot-scope="{ item }">{{ item.name }}</template>
Errorlisteners$listeners is removed1
Event listeners are now part of $attrs. v-on="$listeners" can usually be dropped when v-bind="$attrs" is present.
-
src/components/BaseInput.vue:4:49$listeners is removed: listeners are now included in $attrs.
<input :value="value" v-bind="$attrs" v-on="$listeners" @input="$emit('input', $event.target.value)" />
Errorchildren$children is removed1
Use template refs to reach child component instances.
-
src/components/Modal.vue:21:12$children is removed: use template refs to access child components.
this.$children.forEach((child) => child.reset && child.reset())
Errordestroy$destroy is removed1
Components can no longer destroy themselves. Unmount the app with app.unmount() or remove the component with v-if.
-
src/components/Modal.vue:22:12$destroy is removed: unmount the app or remove the component with v-if.
this.$destroy()
Errorglobal-prototypeVue.prototype is replaced by app.config.globalProperties1
Move Vue.prototype.$http = http to app.config.globalProperties.$http = http, or use provide/inject.
-
src/main.js:27:1Vue.prototype is removed: use app.config.globalProperties or provide/inject.
Vue.prototype.$api = api
Errorconfig-key-codesVue.config.keyCodes is removed1
Custom key code aliases are gone. Use kebab-case key names such as @keyup.page-down.
-
src/main.js:14:1Vue.config.keyCodes is removed: use kebab-case key names in v-on modifiers.
Vue.config.keyCodes = { f2: 113 }
Errorsync-modifier.sync is replaced by v-model:prop1 auto fixable1
:title.sync="value" becomes v-model:title="value". The child keeps emitting update:title.
-
src/components/ProductList.vue:6:23 auto fix.sync is removed: use v-model:product="selected" instead.
<product-card :product.sync="selected" :title="product.name | capitalize" />
Errorfunctional-componentsFunctional components changed1
The functional attribute on <template> and the functional: true option are gone. Use a normal component (the performance gap is negligible) or a plain function.
-
src/components/BaseButton.vue:1:11<template functional> is removed: use a regular component (or a plain function).
<template functional>
Errorinline-templateinline-template is removed1
Move the inner markup into the component itself, or pass it through a default slot.
-
src/components/LegacyWidget.vue:3:19inline-template is removed: move the markup into the component or pass it as a slot.
<legacy-panel inline-template>
Errortemplate-v-for-keyKey must be on <template v-for>1
With <template v-for>, the key belongs on the <template> tag. Keys on its children are a compile error in Vue 3.
-
src/components/ProductList.vue:11:13Move this key to the parent <template v-for>: keys on children of <template v-for> are a compile error in Vue 3.
<dt :key="spec.id">{{ spec.label }}</dt>
Errorv-if-v-forv-if now has higher priority than v-for1
On the same element, v-if is evaluated first, so it cannot use the v-for variable. Filter the list in a computed property or move v-if to a wrapper.
-
src/components/ProductList.vue:4:39v-if and v-for on the same element: in Vue 3 v-if runs first and cannot see the loop variable.
<li v-for="product in products" v-if="product.visible" :key="product.id">
Errordata-objectdata must be a function1
Declaring data as a plain object is no longer allowed, including on the root instance. Use data() { return { ... } }.
-
src/components/LegacyWidget.vue:21:3data must be a function: use data() { return { ... } }.
data: {
Errorvue-router-v3vue-router 3 does not support Vue 31
Upgrade to vue-router 4: new VueRouter() becomes createRouter(), mode becomes history: createWebHistory(), and catch-all routes need a custom regex.
-
package.json:13:5vue-router ^3.5.4 only works with Vue 2: upgrade to vue-router 4 (createRouter, createWebHistory).
"vue-router": "^3.5.4",
Errorvuex-v3Vuex 3 does not support Vue 31
Upgrade to Vuex 4 (createStore), or move to Pinia, the official recommendation for new Vue 3 code.
-
package.json:14:5vuex ^3.6.2 only works with Vue 2: upgrade to Vuex 4 (createStore) or migrate to Pinia, the recommended store for Vue 3.
"vuex": "^3.6.2"
Errorvue2-librariesVue 2 only library1
This library only supports Vue 2. Each finding names its Vue 3 successor.
-
package.json:10:5bootstrap-vue ^2.23.1: BootstrapVue only supports Vue 2. Migrate to bootstrap-vue-next (Bootstrap 5) or plain Bootstrap 5 markup.
"bootstrap-vue": "^2.23.1",
Warningtransition-classesTransition classes renamed4 auto fixable4
.v-enter is now .v-enter-from and .v-leave is now .v-leave-from. The enter-class and leave-class props are now enter-from-class and leave-from-class.
-
src/App.vue:34:1 auto fix.fade-enter is renamed to .fade-enter-from in Vue 3.
.fade-enter, -
src/components/Modal.vue:38:1 auto fix.list-enter is renamed to .list-enter-from in Vue 3.
.list-enter, -
src/styles/transitions.scss:2:1 auto fix.v-enter is renamed to .v-enter-from in Vue 3.
.v-enter, -
src/styles/transitions.scss:16:1 auto fix.slide-enter is renamed to .slide-enter-from in Vue 3.
.slide-enter,
Warninghook-eventshook: lifecycle events are renamed1 auto fixable2
Listening to a child lifecycle with @hook:mounted becomes @vue:mounted. $on('hook:...') no longer works.
-
src/components/Modal.vue:2:23 auto fix@hook:mounted is now @vue:mounted.
<div class="modal" @hook:mounted="onChildMounted"> -
src/components/ProductList.vue:48:10$once('hook:beforeDestroy') no longer works: use the lifecycle hook directly, or @vue:beforeUnmount on the parent template.
this.$once('hook:beforeDestroy', () => clearInterval(this.timer))
Warningnative-modifierThe .native modifier is removed2
Listeners not declared in the child emits option are added to its root element. Remove .native and declare the events the child really emits.
-
src/App.vue:3:47.native is removed: declare the events the child emits with "emits", other listeners fall through to its root element.
<the-header :cart-count="cartCount" @click.native="toggleMenu" /> -
src/legacy/inline.js:7:28.native is removed: declare the events the child emits with "emits", other listeners fall through to its root element.
template: '<button @click.native="count++">{{ count | number }}</button>',
Warningv-bind-object-orderv-bind="object" is order sensitive2
Attributes declared before v-bind="obj" can now be overridden by the object. Move v-bind="obj" first if the explicit attributes should win, as they did in Vue 2.
-
src/components/BaseInput.vue:4:27v-bind="$attrs" comes after value: in Vue 3 the object now overrides those attributes. Move it first to keep Vue 2 behavior.
<input :value="value" v-bind="$attrs" v-on="$listeners" @input="$emit('input', $event.target.value)" /> -
src/components/LegacyWidget.vue:9:37v-bind="widgetAttrs" comes after id: in Vue 3 the object now overrides those attributes. Move it first to keep Vue 2 behavior.
<div id="widget" class="widget" v-bind="widgetAttrs"></div>
Warningvue2-toolingVue 2 build or test tooling2
Compilers, loaders and test utilities are versioned with Vue. Each one has a Vue 3 replacement.
-
package.json:18:5@vue/test-utils ^1.3.6: @vue/test-utils 1 is for Vue 2: upgrade to @vue/test-utils 2.
"@vue/test-utils": "^1.3.6", -
package.json:19:5vue-template-compiler ^2.6.14: vue-template-compiler is Vue 2 only: remove it, Vue 3 ships @vue/compiler-sfc.
"vue-template-compiler": "^2.6.14"
Warningconfig-production-tipVue.config.productionTip is removed1 auto fixable1
The option no longer exists. The statement can simply be deleted.
-
src/main.js:11:1 auto fixVue.config.productionTip is removed: delete this line.
Vue.config.productionTip = false
Warningconfig-ignored-elementsVue.config.ignoredElements is replaced by isCustomElement1
Use compilerOptions.isCustomElement in your build config (or app.config.compilerOptions.isCustomElement with the runtime compiler). It takes a function instead of strings or regular expressions.
-
src/main.js:13:1Vue.config.ignoredElements is removed: use compilerOptions.isCustomElement (a function).
Vue.config.ignoredElements = ['ion-icon']
Warningglobal-configVue.config moved to app.config1
Global configuration now lives on app.config. Some options (silent, devtools) were removed.
-
src/main.js:12:1Vue.config.devtools is not available in Vue 3: use app.config on the instance returned by createApp().
Vue.config.devtools = true
Warningis-on-elementis on native elements needs the vue: prefix1
<tr is="my-row"> now creates a customized built-in element. Use is="vue:my-row" to render a Vue component.
-
src/components/LegacyWidget.vue:7:11is="product-row" on <tr> creates a customized built-in element in Vue 3. Use is="vue:product-row" to render a component.
<tr is="product-row" v-for="row in rows" :key="row.id" :row="row"></tr>
Warningvue-versionVue 2 dependency1
Upgrade vue to version 3. During the migration, alias vue to @vue/compat to run Vue 2 code on Vue 3.
-
package.json:12:5vue ^2.6.14 is Vue 2: upgrade to vue@^3.5. To migrate step by step, alias vue to @vue/compat first.
"vue": "^2.6.14",
Infotransition-group-root<transition-group> renders no wrapper1
Without a tag prop, <transition-group> no longer renders a <span>. Add tag="span" if your CSS relies on it.
-
src/components/Modal.vue:3:5<transition-group> without a tag prop no longer renders a wrapper <span>.
<transition-group name="list">
Infoattrs-class-style$attrs now includes class and style1
Check components that forward $attrs, especially with inheritAttrs: false, because class and style now travel with them.
-
src/components/BaseInput.vue:4:35$attrs now also contains class and style.
<input :value="value" v-bind="$attrs" v-on="$listeners" @input="$emit('input', $event.target.value)" />
Infowatch-arrayArray watchers need deep for mutations1
Watching an array only fires when the array is replaced. Add deep: true if you rely on push, splice and friends.
-
src/components/ProductList.vue:38:5"products" is an array: this watcher only fires when the array is replaced. Add deep: true to react to mutations.
products(list) {