Draft: Move main.js Vue apps to Vue 3 behind a flag
What does this MR do and why?
main.js runs on every page, and every Vue app it mounts is locked to Vue 2. The vue3_migration.yml mechanism cannot switch it, because main is prepended to every page entry and has no bundle of its own. A standalone main entry would fix that, but it changes the chunk graph of every page.
This MR moves all of main.js's Vue apps in one step, with no bundler config change. It uses Option 2 of the Vue 3 migration guide:
main_app.jsholds every Vue app thatmain.jsmounts: behaviors, user and work item popovers, the version check, the session expiry modal, panel resizers, and the EE trial banner and SAML modal.- When
vue3_migrate_mainis on,main.jsimportsmain_app.jswith?vue3. If that import fails, the Vue 2 copy runs and the error goes to Sentry. - With the flag off,
main.jscalls the Vue 2 copy synchronously, at the same points as before.
Gitlab::GonHelper#add_gon_feature_flags pushes the flag with the current_user actor, because main.js runs on every page.
Nothing may run on import
The Vue 2 copy of main_app.js is a static import, so it is evaluated even when the flag is on. Any module below it that does work on import would do that work twice, once per runtime. Three did:
behaviors/index.jsran its inits at module scope.toggler_behavior.jsregistered its jQuery handlers at module scope.main_ee.jsran the trial banner and SAML modal inits at module scope.
They now export init functions, and main_app.js calls them. Their side-effect imports that are Vue-free or blocklisted stay, because those have one copy. main.js keeps import './behaviors' in its old position, so module evaluation order does not change.
Shared modules stay on one copy
A Vue 3 copy of a module does not share module state with its Vue 2 copy. Vue 2 page code imports some of the same modules as main, so this MR adds them to INFECTION_BLOCKLIST. A blocklisted module always builds as one Vue 2 copy:
tooltips/index.jsandpopovers/index.js: one app thatmain.jsstarts and page code callsbehaviors/shortcuts/index.js: oneShortcutsinstance. Two copies bind every key twice.vue_shared/plugins/global_toast.js: one toast rootbehaviors/markdown/render_iframe.jsandrender_sandboxed_mermaid.js: aWeakMapthat stops an element from rendering twicelib/utils/axios_utils.js: interceptors registered on the oneaxiosinstance
These entries also apply to pages that already roll out through vue3_migration.yml. On those pages they remove existing duplicates. For example, axios interceptors stop running twice per request.
How the list was found
- I simulated the change with the infection scanner's lane walk and the loader's real
isInfectablerule:- Vue 2 lane:
main.jswithout the moved code, every page entry, and the always-loaded bundles - Vue 3 lane: the moved code
- Vue 2 lane:
- Every module present in both lanes with module-level state was triaged by hand. These are harmless as two copies, so they are not blocklisted:
emoji(a cache)issuable/popover(a debounce timer; the mounted flag lives in the DOM)text_markdown(an IME flag per listener)apollo_captcha_link(a stateless link)internal_events(a bind guard)
- A Babel pass then listed every module in the Vue 3 lane that runs code on import. Only module-local assignments remain:
gfm_auto_completein CE and EE,markdown/schema.js, andvue3compat/vue.js, which is per runtime by design. - The state detection is pattern-based. It missed
shortcuts, which I found by reading the code. This pipeline runs every feature spec with the flag on, and that is the real test.
Result
- About 49 components move to Vue 3.
- 115 components below the moved code stay on Vue 2. All of them are held back by the existing
behaviors/preview_markdown.jsentry (glql, content_editor and others), exactly as on rollout pages today. - The Vue 2 runtime stays on every page, because
~/commonsand Vue 2 page code still need it.
What was ruled out
- One app at a time, which was the first version of this MR. It never reaches "main on Vue 3".
- A switch inside the bundle, with static
requireof both copies. It ships both copies to every page and does not work under Vite. - A
main.vue3entry. It needs the standalonemainentry above, or about 2,500 page entries.
Screenshots or screen recordings
Not recorded yet.
How to set up and validate locally
-
Enable the flag in the Rails console:
Feature.enable(:vue3_migrate_main) -
Open a project page, such as
http://127.0.0.1:3000/gitlab-org/gitlab-test. -
In the console, check for
[gitlab] [V] Using Vue.js 3 (with @vue/compat) for PanelWidthResizerRoot. -
Run
document.querySelectorAll('[data-gitlab-vue3-app]'). It returns the panel resizer roots. -
Check that nothing runs twice:
- Press
?. One shortcuts help modal opens. - Hover a link with a tooltip, then click it. The tooltip hides.
- Click a collapsible section toggle. It opens once.
- Press
-
Hover a user link. The user popover opens.
-
Disable the flag and reload. The console shows no Vue 3 notice for these roots.
Local checks run:
yarn jest spec/frontend/config/vue3migration spec/frontend/scripts/infection_scanner spec/frontend/behaviors: 36 suites, 551 passed, 1 skipped.node scripts/frontend/infection_scanner/infection_scanner.mjs: duplicated modules check, 685 page states, none found.- ESLint on every changed file.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
References
- Part of #623999 — Vue 3 migration of global bundles and
main - Rollout issue: #633362
- Alternative that changes the chunk graph: !252290 (closed)
- Shared tooltips and popovers state: #628699
- Build-time duplicated-modules check: !252666 (merged)