Page MenuHomePhabricator

Prepare Vue 2 to Vue 3 migration
Open, HighPublic

Description

Proposed migration plan as of July 23, 2021 (some of the comments below may be contradictory and confusing, because I was working my way to this and hit some dead ends):

Based on all that, I think our migration plan could look something like:

  1. Write a wrapper for mounting components that mocks something like the Vue 3 API for Vue 2, and that mounts the component as a child of the given element, rather than replacing the element (i.e. the Vue 3 behavior rather than the Vue 2 behavior). For a draft of this wrapper, see here.
  2. Make all MW code that mounts components use this wrapper.
  3. Upgrade the version of Vue that is bundled with MediaWiki (and exposed as the vue ResourceLoader module) from version 2.6 of vue to version 3.2(*) of @vue/compat (once it's out), configured for Vue 2 mode. At the same time, change the wrapper to use the Vue 3 createApp API, and monkey-patch new Vue() and new Vue().$mount() to behave like Vue 2 rather than Vue 3 (for some reason, the Vue 3 compat build doesn't provide compatibility here). For a draft of what the wrapper would look like at this point, see here.
  4. Check that everything still works, and make minimal changes to fix things where necessary. There will be a million migration warnings, but that's OK.
  5. Change all code that uses Vuex to use the createMwApp() wrapper. This is necessary because Vuex 4 doesn't support new Vue( { store: store, ... } ), not even when using the Vue 3 compat build, and there seems to be no way for us to monkey patch that compatibility in either.
  6. Upgrade Vuex from 3.1.3 to 4.0.2 (or whatever the latest version is by then). Make the createMwApp() wrapper backwards compatible, so that createMwApp( { ..., store: store } ) still works. (Vuex 4 instead requires createMwApp( { ... } ).use( store ).) For a draft of this wrapper code, see here.
  7. Migrate things one by one, addressing their migration warnings until there aren't any left. This means migrating individual apps, whether they're using a build step or not, and the component library. These can be migrated piecemeal in any order.
    • Once a component is migrated, it should set componentConfig: { MODE: 3 } in its component options. This ensures that compat features that get in the way of migrated code (such as ATTR_FALSE_VALUE) are disabled, and helps test that the component will work correctly in Vue 3.
    • The component library can be migrated to Vue 3 directly (using the compat build as an aid, but without ever shipping a release of the library that uses Vue 3-compat), or built in Vue 3 from the ground up. Either way, it will need to set compatConfig: { MODE: 3 } on every component to make things work in MediaWiki as long as MW still uses the compat build. This could be done in the library or in MW's wrappers for the library.
  8. Once everything is migrated, try setting the global compatConfig to { MODE: 3 }, to test that everything really is migrated.
  9. Remove the global Vue.use( i18n ) calls, and change the i18n plugin to use the Vue 3 API (doing this earlier would make the i18n plugin unavailable to code using the Vue 2 mounting API).
  10. Switch the Vue build in MediaWiki from @vue/compat to vue. We're now running real Vue 3, without any compat behavior.
  11. At our leisure, remove compatConfig from everything.

(*) Version 3.2 is needed because it fixes a bug that breaks the i18n plugin

Related Objects

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes

Change 666460 had a related patch set uploaded (by Catrope; owner: Catrope):
[mediawiki/core@master] [WIP] Add Vue.createMwApp(), to help with Vue 2->3 migration

https://gerrit.wikimedia.org/r/666460

Change 666434 had a related patch set uploaded (by Catrope; owner: Catrope):
[mediawiki/core@master] [WIP] Proof of concept Vue 3 migration

https://gerrit.wikimedia.org/r/666434

Here's my first stab at a proposal for how we'd provide a smooth migration experience:

We would encourage/require all code that mounts a Vue component to the DOM to use this pattern:

var RootComponent = require( './RootComponent.Vue' ),
    store = require( './store.js' ); // omit if not using Vuex

Vue.createMwApp( RootComponent )
    .use( store ) // omit if not using Vuex
    .mount( '#container' );

This is almost identical to how mounting components works in Vue 3, except with Vue.createApp() instead of createMwApp(). While we're still using Vue 2, createMwApp() would be a shim that returns an object that pretends to be a Vue 3 app, but really only supports .use() and .mount() (implementation here).

After we migrate to Vue 3, createMwApp() would become a much more straightforward wrapper around Vue.createApp() (implementation here). We would continue to use it, because we need the i18n plugin and the error handler to be injected. Currently (in Vue 2), these are injected into the global Vue object once, then automatically reused. But Vue 3 got rid of this global state, so they have to be injected separately into every new app object.

Other notes:

  • Vue 3 is only distributed as an ES6 library, there's no ES5 build. That means that the vue module will need to be an ES6 module, and so this migration depends on T272882: Upgrade ResourceLoader JS minifier to support ES6 and T272104: Allow modules to opt-in to ES6 syntax support
  • There is no compiler-only build that can be loaded in later to augment the runtime-only build. However, it appears to be pretty easy to make one (either upstream, or on our own if upstream doesn't accept our suggestion)
  • For Vuex, the documentation recommends using store = Vuex.createStore( { ... } ); which is new in Vuex 4, but the Vuex 3 pattern of store = new Vuex.Store( { ... } ); still works. So we should continue to use the latter until we have migrated to Vuex 4.
  • I'm assuming that components written for Vue 2 will typically be compatible with Vue 3 without modification; or alternatively, that it would be easy to modify them to make them compatible. I've tested my Vue 2->3 migration against a simple Vue+Vuex application, but I'll test it against our other existing Vue applications as well to make sure
  • Similarly, I think making component libraries (such as WVUI and Wikit) compatible with both Vue 2 and Vue 3 will be easy; this is also less important, because it would be more feasible to have Vue 2 and Vue 3 branches/builds there

Change 666980 had a related patch set uploaded (by Catrope; owner: Catrope):
[mediawiki/core@master] [WIP] Proof of concept for Vue 2 / Vue 3 coexistence

https://gerrit.wikimedia.org/r/666980

@Tonina_Zhelyazkova_WMDE told me yesterday that WMDE wants to continue to support IE11 for its Vue features. Since Vue 3 still doesn't support IE11 (it may eventually, but the timeline for it seems to keep slipping), this means WMDE-maintained features would need to stay on Vue 2 for a while after other features are migrated to Vue 3. In turn, that means that we'd need to have Vue 2 and Vue 3 coexist somehow.

It turns out that Vue 2 and Vue 3 can in fact coexist on the same page without any issues, as can Vuex 3 and Vuex 4. For performance reasons, we obviously want to avoid loading multiple versions of Vue on the same page as much as possible. That won't happen often (more about this in the next comment), but it may sometimes be necessary. Here's how I think that could work, based on the proof of concept in this patch:

  • We'd add ResourceLoader modules called vue2 and vuex3. Initially, these would just be aliases for vue and vuex respectively.
  • Code that is not ready to migrate and wants to stay on Vue 2 would use these, by using var Vue = require( 'vue2' ) instead of require( 'vue' ) (and require( 'vuex3' ) instead of require( 'vuex' ))
  • Code that is ready to migrate to Vue 3 once it's available would keep using require( 'vue' ) and require( 'vuex' ), and would use the Vue.createMwApp() API to mount components (as outlined above)
  • When we migrate, we would change the vue module to contain Vue 3, and the vuex module to contain Vuex 4. Code that uses these modules will start using the new versions transparently, and their mounting code will keep working thanks to the Vue.createMwApp() compatibility polyfill
  • In some situations (hopefully rarely), a Vue 2 and a Vue 3 feature will load on the same page, and both versions of the library will need to be loaded. This isn't the best for performance, but as far as I can tell from testing and inspecting the code, it runs just fine (though the dev tools in the browser inspector may break when the second Vue version is loaded)

When thinking about how often Vue 2 and Vue 3 code would load on the same page, I divided front-end features into four categories based on where they're loaded:

A. On a special page (e.g. Special:MediaSearch, Special:ContentTranslation etc.), typically not lazy-loaded
B. On pages with a custom content type (e.g. Wikidata entity pages, WikiLambda ZObject pages), typically not lazy-loaded
C. On wikitext pages, sometimes lazy-loaded (e.g. Wikidata Bridge)
D. On every page, typically lazy-loaded (e.g. the new Vector search)

The only combinations that can load at the same time are two features in category C, or a feature in category D with another feature (in any category). If, for example, all current WMDE features use Vue 2, and all current and new WMF features use Vue 3, then the only cases where we'll load both versions of Vue are:

  • When we're on a Wikidata entity page (or perhaps a Wikidata-related special page) and the user interacts with the search bar or another category D feature that lazy-loads on user interaction
  • When we're on a wikitext page that has both Wikidata facts that can be edited inline through Wikidata Bridge and some other Vue-based feature, and both are loaded (either because the user interacts with them, or because they always load)

Making Wikidata Bridge lazy-load on user interaction would further reduce the cases where this happens; right now Wikidata Bridge loads immediately after page load if it detects that there are Wikidata facts on the page.

Another random note on Vue 3: right now, Vue devtools support for Vue 3 is still in beta, and the version of the devtools that supports Vue 3 does not support Vuex yet and does not have hybrid support for both Vue 2 and Vue 3. Hopefully this will be fixed in the future. It seems unlikely to me that the devtools would be able to deal with both Vue 2 and Vue 3 loading on the same page, but they'd be pretty useless if they didn't support inspecting one page with Vue 3, then a different page with Vue 2.

Rereading the (now more detailed) Vue 3 migration guide, I noticed some more things:

  • My approach for the createMwApp() wrapper won't work as well as I thought, because Vue 2 and Vue 3 not only have different mount APIs but also mount components differently: Vue 2 replaces the mounted element whereas Vue 3 puts the component inside of the element instead. The wrapper would have to account for that somehow. But we can probably make that work, and there are other reasons to want a mounting wrapper, like injecting the i18n plugin.
  • Fortunately, there's a compat build that comes along with Vue 3.1, which supports both the old and the new mount API when used in compat mode. It also supports most other Vue 2 features, and I think we should try to use it for our migration instead of trying to come up with our own wrappers.
  • There are some things the compat build can't paper over, like the fact that :some-attribute="false" behaves differently in Vue 2 (attribute is removed) vs Vue 3 (attribute is set to the string 'false'). The compat plugin uses the Vue 2 behavior by default, but that could break Vue 3 code (it can be configured to use the Vue 3 behavior instead, either globally or per-component, and either for everything or just this one behavior). We'll probably be able to work around this by not writing code that would be affected by this problem, but things like this will complicate the migration.
  • The fact that vm.$on() was removed in Vue 3 will be an issue, because it's used in two places in Wikidata Bridge. This will also impair our ability to glue together Vue and non-Vue code, because we won't be able to listen for the events emitted by a Vue component as easily. We can probably work around this using wrapper components that handle events
  • There are significant breaking changes in the render function API between Vue 2 and Vue 3. Code generated by the Vue 2 template compiler almost certainly wouldn't run in vanilla Vue 3. However, based on some cursory testing (using the button component from WVUI) it does appear to work with the compat plugin! This is great news, because it means that we could (in theory) switch MediaWiki's version of Vue from 2.6 to 3.1 + the compat plugin, and existing generally shouldn't break, even if it was compiled with a build step that uses Vue 2.6 from NPM.

Two more things I discovered:

  • The Vue 2 plugin API also works in the migration build, so we wouldn't even have to migrate our plugins immediately. I tested that our i18n plugin works out of the box.
  • The migration build isn't really a plugin, it's a separate build of Vue. I was thrown off by the fact that @vue/compat declares vue as a peerDependency, but in reality the @vue/compat contains all of Vue plus the compatibility stuff. For every file in the dist directory of the vue package, there's a corresponding file in the dist directory of the @vue/compat package, so we can just change the package name without needing to do anything else.

(New migration plan, moved to task description)

Once a component is migrated, it should set componentConfig: { MODE: 3 } in its component options

Instead of doing this, I am wondering if there is a Vue 3 only method that could be used instead (with a compatibility layer for Vue 2). Perhaps createApp for example?

// Vue 3 ready
const app = Vue.createApp({
  /* options */
})

// Not Vue 3 ready
const app = new Vue( { } )

Something like:

// Vue 2.

( function () {
        var Vue = require( '../../lib/vue/vue.js' );

       Vue.createApp = function ( options ) {
            return new Vue( options );
       }
        module.exports = function () {
             mw.log.warn('This Vue component needs to be migrated to Vue3');
             return Vue.apply( Vue, arguments );
       };
}() );
Catrope updated the task description. (Show Details)

Once a component is migrated, it should set componentConfig: { MODE: 3 } in its component options

Instead of doing this, I am wondering if there is a Vue 3 only method that could be used instead (with a compatibility layer for Vue 2). Perhaps createApp for example?

We could do that in addition, if we wanted to produce deprecation warnings for code that tries to use Vue 2. But that's not the main purpose of putting componentConfig: { MODE: 3 } in the component options; what that does is tell the Vue compatibility layer to behave like Vue 3 without any compat stuff. This is necessary so that 1) any attempts to use Vue 2 behavior break (giving us confidence that moving from the compat build to the regular build is safe), and 2) in situations where Vue 2 and Vue 3 behave differently for the same code, the Vue 3 behavior is used instead of the Vue 2 behavior.

An example of #2 is what happens when you do <div :aria-disabled="foo">, and the value of foo is the boolean false. In Vue 3, this sets the aria-disabled attribute to the string 'false', but in Vue 2, it removes the attribute (see also this entry in the migration guide). The migration build can't provide support for both the Vue 2 and Vue 3 way of doing this, it has to choose one of these two behaviors. By default, it chooses the Vue 2 behavior. This makes sense for unmigrated components (so they keep working unchanged), but once a component is migrated it will want/need to get the Vue 3 behavior instead. It can do this by setting either compatConfig: { ATTR_FALSE_VALUE: false } (to turn off compat for just that one feature) or compatConfig: { MODE: 3 } (to switch of all compat and get Vue 3 behavior for everything).

I like the idea of throwing a warning when unmigrated code is run, as a way for us to track down things that haven't been migrated yet. But I don't think we can do it the way you suggested, because:

  • The behavior of .$mount() is incompatible, even when using the Vue 2 calling signature, which means we have to wrap/mock it before and during the migration
  • Vue 3 doesn't allow for global plugin registration, instead you have to inject plugins every time after calling createApp(), which means we need to wrap Vue.createApp() during and after the migration

The most elegant way to solve both of those problems at once seemed to me to be a Vue.createMwApp() wrapper that emulates the Vue 3 behavior pre-migration, and injects plugins post-migration. But I'm now realizing we could also monkey patch or wrap the Vue constructor and the .$mount() method to fix the lack of compatibility and provide the Vue 2 behavior during the migration; I'll experiment with that.

I had some fun learning about ES6 proxies, and I've updated the attached Gerrit patch. I managed to implement a wrapper around Vue 3's new Vue() and new Vue().$mount() that makes them behave like they do in Vue 2, which should make migration easier. This way, migrating everything to use createMwApp() first before switching from Vue 2 to the Vue 3 migration build won't be required.

Change 709125 had a related patch set uploaded (by Catrope; author: Catrope):

[mediawiki/core@master] [WIP] Upgrade Vuex to 4.0.2

https://gerrit.wikimedia.org/r/709125

Catrope renamed this task from Pre-plan Vue 2 to Vue 3 migration to Prepare Vue 2 to Vue 3 migration.Aug 16 2021, 10:15 PM

A question that I'd like feedback/input on: this plan proposes adding Vue.createMwApp(), and keeping it as a wrapper around Vue.createApp() indefinitely, even in the post-migration future. All this wrapper would do is inject the error handler and the i18n plugin. During part of the migration, this wrapper would also provide compatibility as we upgrade from Vuex 3 to Vuex 4. But maybe there are alternative approaches where we don't have to keep this wrapper around.

For example, we could monkey-patch Vue.createApp() to always inject the error handler and the i18n plugin. That way extension code couldn't accidentally do the wrong thing by calling createApp instead of createMwApp, but they also couldn't opt out of anything. Alternatively, we could monkey-patch Vue.createApp() to inject the error handler and provide Vuex compatibility, but make individual callers inject the i18n plugin themselves. This would make the developer responsible for explicitly including the things that change Vue's behavior and produce noticeable errors when incorrectly omitted, but would automatically include the things that have a more minor/subtle impact but are hard to notice if you forgot them.

What end user code would look like under each of these alternatives:

// Option 1: createMwApp wrapper
const App = require( './App.vue' );
Vue.createMwApp( App ) // PITFALL: if you use createApp instead, i18n stuff breaks (loudly) and error logging breaks (silently)
    .mount( '#selector' );

// Option 2: monkey-patch createApp
const App = require( './App.vue' );
Vue.createApp( App )
    .mount( '#selector' );

// Option 3: minimally monkey-patch createApp
const App = require( './App.vue' );
const i18n = require( 'vue-i18n' );
Vue.createApp( App )
    .use( i18n ) // PITFALL: if you forget to do this, i18n stuff breaks (loudly)
    .mount( '#selector' );

IMHO, the first option is by far the best one:

  • it is obvious to the developer that some mediawiki magic is at play here, which is much better than having to expect any third party code to have been manipulated
    • i.e. explicit being usually better than implicit
  • related to the above: it is searchable
    • searching for "Vue.createMwApp" will probably have its mediawiki.org documentation as the first search result, but good look figuring out what a monkey-patched is Vue.createApp is doing
  • it can be easily opted out of, if it is not doing exactly what is needed
    • that could be for local dev environment, (browser) testing, running the code on 3rd party wikis with different infrastructure, or making use of some upcoming (beta-)feature on the wmf infrastructure
  • for new developers that are introduced to working with Vue.js in a MediaWiki context, no wrong expectations are created that would lead to confusion when trying to use vuejs in a context that is not directly on-wiki

These are all great points, and I find them convincing that option 1 is the best way to go. Someone also pointed out to me that we could have a lint rule (or other CI thing) that warns you against using Vue.createApp() in MediaWiki code, as a way to combat the pitfall in option 1. (Which matters not only because the i18n plugin won't work, but also because error logging will silently fail.)

Change 724139 had a related patch set uploaded (by Catrope; author: Catrope):

[mediawiki/core@master] Add Vue composition API plugin

https://gerrit.wikimedia.org/r/724139

Change 666980 abandoned by Catrope:

[mediawiki/core@master] [WIP] Proof of concept for Vue 2 / Vue 3 coexistence

Reason:

https://gerrit.wikimedia.org/r/666980

Change 666460 merged by jenkins-bot:

[mediawiki/core@master] Add Vue.createMwApp(), to help with Vue 2->3 migration

https://gerrit.wikimedia.org/r/666460

Change 724139 merged by jenkins-bot:

[mediawiki/core@master] Add Vue composition API plugin

https://gerrit.wikimedia.org/r/724139

Change 666434 merged by jenkins-bot:

[mediawiki/core@master] Upgrade Vue to the migration build of Vue 3

https://gerrit.wikimedia.org/r/666434

Jdlrobson added a subscriber: Edtadros.

During QA @Edtadros noticed an issue with search results not being clickable that appears to be related. @Catrope is looking into it.
{

screen_recording_2021-12-08_at_4.11.58_pm.mov.gif (470×868 px, 281 KB)
}

During QA @Edtadros noticed an issue with search results not being clickable that appears to be related. @Catrope is looking into it.
{

screen_recording_2021-12-08_at_4.11.58_pm.mov.gif (470×868 px, 281 KB)
}

@Catrope - would it be easier if we tracked this under a separate ticket? I'm open to both, just want to make sure this bug doesn't make it on the train somehow

Yes, a separate task would be best. I'll file one.

Now that MediaSearch is able to successfully run in the compatibility build, I started looking at what further changes would be needed to get full Vue 3 behavior.

I'm starting to wonder if it would simplify things if we provided a non-customized copy of Vue 3 and exposed things like the i18n plugin as stand-alone resource modules so that consuming applications can compose themselves instead having various plugins pre-bundled.

This could allow us to write add conditional logic inside of plugins to detect whether code is running in compat mode or not, and choose between V2-style prototype manipulation to V3-style app.config.globalProperties based on that. Similarly if we exposed Vuex 4 as a separate module, individual apps could switch over their Vuex version (which also needs to be initialized differently) when ready.

Realistically some apps will probably be able to migrate much sooner than others and it would be great to remove any blockers at the global level.

Jdforrester-WMF lowered the priority of this task from Unbreak Now! to High.Wed, Jan 19, 9:03 PM
Jdforrester-WMF added a subscriber: Jdforrester-WMF.

Whatever else, this isn't UBN! right now. :-)

Should this be merged into T289017: [Epic] Migrate from Vue 2 to Vue 3, or just marked as Resolved as the 'prepare for' bit is done?