Draft: Guard modules that must stay shared across Vue 2 and Vue 3
What does this MR do and why?
GitLab is migrating to Vue 3 with an infection system. A module gets duplicated per runtime only if its import subtree reaches a Vue-family specifier. A module with no Vue below it stays shared by both runtimes.
Three modules must stay shared:
ee/app/assets/javascripts/ai/events/panel.js(the Duo panel event hub)app/assets/javascripts/helpers/event_hub_factory.jsee/app/assets/javascripts/ai/duo_agentic_chat/context/external_context_store.js
Page code in work_items, boards, vue_merge_request_widget, and ci/pipeline_details emits on the Duo panel hub. Several of these pages already run Vue 3 behind rollout flags. The hub already crosses the Vue 2 / Vue 3 boundary today, and it works only because it is shared.
This safety is not declared anywhere. It is emergent. One Vue-touching import added anywhere under these three modules splits the singleton into two copies. panel.js also holds a module-level pendingScrollToSessions flag. That flag would split along with the hub.
A duplicated hub still builds. It still accepts emits. The emits just stop reaching listeners on the other runtime. Nothing fails loudly.
This MR adds a build-time guard against that failure mode:
config/helpers/context_aliases_shared.jsadds anINFECTION_MUST_STAY_SHAREDlist, next to the existingINFECTION_BLOCKLIST, naming the three modules above.scripts/frontend/infection_scanner/analyze.mjsadds an exported pure function,findSharedModuleViolations(graph, mustStayShared, fileExists). It reports two cases:infected: the module gained a Vue dependency. The reasons are included.unreachable: the module exists on disk, but no entrypoint imports it, so the check would prove nothing. Paths with no file on disk are skipped, so a FOSS build does not fail onee/entries.
scripts/frontend/infection_scanner/infection_scanner.mjscalls the new function duringrunAnalysis. It throws, naming the offending module and the import that pulled Vue in.spec/frontend/scripts/infection_scanner/analyze_spec.mjsadds five unit cases for the new function.
The guard lives in the scanner, not in the test-infection-scanner vitest job. WebpackVue3InfectionPlugin.apply() runs the scanner on every asset build. runInfectionScanner turns a non-zero exit into a build error in CI, and into a warning locally. The vitest job cannot carry this check alone: its changes rule in .gitlab/ci/rules.gitlab-ci.yml only fires for changes under scripts/frontend/infection_scanner/** and spec/frontend/scripts/infection_scanner/**. It would never see an import added to one of the guarded modules.
How to verify
-
Run the unit tests:
yarn vitest:infection-scanner. All 136 tests pass. -
Run the scanner on an unmodified tree:
yarn infection-scanner. It exits 0 in about 3 seconds. -
Negative test: add
import Vue from 'vue';toapp/assets/javascripts/helpers/event_hub_factory.js, then run the scanner again. It exits non-zero with this message:Error: [vue3-infection-scanner] Modules that must stay shared across Vue 2 and Vue 3 have changed: ee/app/assets/javascripts/ai/events/panel.js now reaches Vue, so it is duplicated per runtime. via app/assets/javascripts/helpers/event_hub_factory.js (vue) Either drop that import, or move the singleton behind `~/lib/utils/observable`.Revert the import after the check.
Limitations
The guard proves that a module is not duplicated. It does not prove that the singleton is used correctly. It is a tripwire on a property that is currently accidental, not a correctness proof for the shared modules.
References
- #623999 — tracking issue; this MR is Phase 0 of that plan
- !252041 (closed) — Duo panel Vue 3 prototype that exposed this risk
- &6252 — parent epic, Migration from Vue 2 to Vue 3