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.js
  • ee/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.js adds an INFECTION_MUST_STAY_SHARED list, next to the existing INFECTION_BLOCKLIST, naming the three modules above.
  • scripts/frontend/infection_scanner/analyze.mjs adds 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 on ee/ entries.
  • scripts/frontend/infection_scanner/infection_scanner.mjs calls the new function during runAnalysis. It throws, naming the offending module and the import that pulled Vue in.
  • spec/frontend/scripts/infection_scanner/analyze_spec.mjs adds 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

  1. Run the unit tests: yarn vitest:infection-scanner. All 136 tests pass.

  2. Run the scanner on an unmodified tree: yarn infection-scanner. It exits 0 in about 3 seconds.

  3. Negative test: add import Vue from 'vue'; to app/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

Merge request reports

Loading
Loading