Fail the build when a page duplicates a module-scope singleton
What does this MR do and why?
A migrated page can load some modules with both Vue 2 and Vue 3, through our "infection" mechanism. As each Vue version runs the same module, modules that contain state can be duplicated, and modules that execute code can run twice, for example registering an event handler twice. We call these modules singletons.
Duplication is hard to catch. A consumer does not see that the module is duplicated. It reads a stale value, and nothing reports an error.
This MR fails the build when it happens. The check runs inside compile-production-assets and build-vite-prod, which both block the pipeline.
[vue3-infection-scanner] Duplicated modules detected: 1 module(s) on 14 page state(s), 14 finding(s).
pages.dashboard.issues (flag on)
roots: app/assets/javascripts/main.js
app/assets/javascripts/pages/dashboard/issues/index.js
app/assets/javascripts/entrypoints/super_sidebar.js
app/assets/javascripts/graphql_shared/issuable_default_client.js
holds: apollo-client
sink: (none: this copy has no Vue 3 ancestor)
V2 app/assets/javascripts/entrypoints/super_sidebar.js
V2 app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
V2 app/assets/javascripts/graphql_shared/issuable_client.js
V2 app/assets/javascripts/graphql_shared/issuable_default_client.jssink names the module to fix, and the error prints paste-ready INFECTION_FORCELIST lines. The full guidance is in the guide, which this MR also updates: doc/development/fe_guide/vue3_migration.md, under Module-scope singletons.
How it works
The scanner walks every migrated page in both lanes at once, and reports a module that appears in both while holding module-scope state.
Three things decide what it walks:
- Page states come from the webpack entry map.
generateEntriesstamps?vue3on the roots of a migrated page, and emits a second<name>.vue3key for a rollout page. So a specifier carrying?vue3seeds the Vue 3 lane and anything else seeds Vue 2. A rollout page is checked in both flag states, because it ships in both. - The global bundles are seeded too.
super_sidebar,tracker,sentryandperformance_barload next to every page bundle and have no.vue3variant, so each is a permanent Vue 2 lane. The list lives inconfig/helpers/entry_points.jsandspec/frontend/config/entry_points_spec.jschecks it against the layouts. - The walk calls the same
isInfectablethe bundlers call. A private copy would keep passing after the real rule changed.
Detection parses each module with @babel/parser and inspects top-level statements only, so a factory that returns a fresh object per call is ignored. A singleton built inside an IIFE, or assigned conditionally, is not detected.
DUPLICATION_EXPECTED lists modules where two copies are correct: a VueApollo provider and a Pinia instance are bound to the Vue version that built them, so each lane has to build its own. There is no list for excusing a page that is known to be broken.
How to verify
node scripts/frontend/infection_scanner/infection_scanner.mjs; echo "exit=$?"
FOSS_ONLY=true node scripts/frontend/infection_scanner/infection_scanner.mjs; echo "exit=$?"
yarn vitest:infection-scanner
yarn jest spec/frontend/config/entry_points_spec.jsThe check passes on 353 EE page states and 240 CE page states. To see it fail, remove graphql_shared/issuable_default_client.js from INFECTION_BLOCKLIST and run the scanner again: it reports 1 module on 14 page states.
No changelog entry, because this is build tooling with no user-facing change.
The merge requests in this group
| MR | What it does | State |
|---|---|---|
| !252664 (closed) | Runs the scanner specs when its config changes | Open, ready |
| !253161 (merged) | Fixes the five duplicated modules this check found | Merged |
| !253294 (closed) | Blocklists three modules that must stay shared | Open, draft |
| This MR | Adds the check | Open, draft |
| !252667 (merged) | Removes the app-root barrier, so the forcelist is no longer needed | Open, draft, stacked on this one |
!253161 (merged) had to land first, because this check reports the duplications it fixes. !252667 (merged) sits last: it widens what gets duplicated, and this check is what makes that cost measurable.
References
- Fixes #625296 (closed)
- Full analysis of the root cause: #625296 (comment 3760799545)
- Supplies the
INFECTION_FORCELISTthis check relies on, merged: !252665 (merged) - The migration this unblocks: !252389 (merged)
- Follow-up to consolidate this tooling into one directory: #626610 (closed)