Consolidate the Vue 3 build tooling into config/vue3/

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

Problem

The Vue 3 build tooling has grown organically and has no single home. It spans four directories, and which one a file landed in depends on which bundler first needed it:

directory Vue 3 files
config/helpers/ context_aliases_shared.js, vue3_infection_shared.js, vue3_migration_loader.js, vue3_migration_file_validation.js, vue_version.js, vite_plugin_vue3_infection.mjs
config/plugins/ webpack_vue3_infection_plugin.js, vue3_migration_manifest_plugin.js
config/rspack/ vue3_infection_loader.js, vue3_infection_resolve_plugin.js
config/vue3migration/ vue2_compiler.js, vue3_template_compiler.js, vue3_sfc_compiler.mjs

This surfaced concretely while reviewing !252664 (closed). That MR widens .frontend:rules:test-infection-scanner so the vitest suite runs when the code it covers changes, and doing so required a hand-listed set of six paths. The list is arbitrary, because there is no glob that means "Vue 3 build tooling". It is already incomplete: vue3_migration_loader.js, vue3_migration_file_validation.js and vue3_migration_manifest_plugin.js are the same concern and are absent from it. The next person to add a file has no reason to know the list exists.

Two secondary smells:

  • Names do not say what layer they are. context_aliases_shared.js and vue3_infection_shared.js both say "shared" without saying shared between what. Word order flips between bundlers: vite_plugin_vue3_infection.mjs against webpack_vue3_infection_plugin.js.
  • Several modules are loaded by string-built paths, for example cjsRequire(path.join(ROOT_PATH, 'config/helpers/context_aliases_shared')), so a rename is caught by neither tooling nor a naive grep.

Proposal

config/vue3migration/ already exists and holds three of these files, so this is a consolidation into a half-used home rather than a new convention. Note it is also the only directory under config/ without word separators in its name.

config/vue3/
  README.md                  new: data flow, entry points, glossary for "infection"
  aliases.js                 <- config/helpers/context_aliases_shared.js
  infection.js               <- config/helpers/vue3_infection_shared.js
  migration.js               <- config/helpers/vue3_migration_loader.js
  migration_schema.js        <- config/helpers/vue3_migration_file_validation.js
  version.js                 <- config/helpers/vue_version.js
  compilers/
    vue2_template.js         <- config/vue3migration/vue2_compiler.js
    vue3_template.js         <- config/vue3migration/vue3_template_compiler.js
    vue3_sfc.mjs             <- config/vue3migration/vue3_sfc_compiler.mjs
  plugins/
    vite.mjs                 <- config/helpers/vite_plugin_vue3_infection.mjs
    webpack.js               <- config/plugins/webpack_vue3_infection_plugin.js
    rspack_loader.js         <- config/rspack/vue3_infection_loader.js
    rspack_resolve.js        <- config/rspack/vue3_infection_resolve_plugin.js
    manifest.js              <- config/plugins/vue3_migration_manifest_plugin.js

Files inside the directory drop the vue3_ prefix, because the directory carries it. compilers/ keeps the version in its filenames, because there the version distinguishes siblings from each other rather than restating the directory.

The CI rule this exists for becomes four location globs instead of eight hand-listed paths, and new files are covered automatically:

changes:
  - "config/vue3/**/*"
  - "scripts/frontend/infection_scanner/**/*"
  - "spec/frontend/config/vue3/**/*"
  - "spec/frontend/scripts/infection_scanner/**/*"

Why a directory rather than a naming convention

Every one of these files already has "vue3" in its name except context_aliases_shared.js, so one rename plus a glob config/**/*vue3* would fix the CI rule with almost no diff. That was considered and rejected: nothing enforces the name, so a name-based glob rots the same way a hand-listed one does, just less visibly. A directory makes membership structural.

The other argument is deletability. This whole subsystem is temporary. When the migration completes, rm -rf config/vue3 is the entire cleanup, instead of hunting fourteen files across four directories.

The tradeoff to be explicit about in review: it diverges from the config/helpers/vite_plugin_*.mjs family, which has 12 members. Feature cohesion is being chosen over bundler cohesion here.

Two silent-failure classes

These carry the review weight, because they break without an error.

1. Depth-sensitive roots. Three moved files derive the repo root from __dirname:

  • context_aliases_shared.js:3 path.resolve(__dirname, '../..')
  • vue3_infection_shared.js:6 path.resolve(__dirname, '..', '..')
  • vue3_migration_loader.js:12 path.resolve(__dirname, '..', '..')

At config/vue3/ the depth is unchanged. Anything landing in config/vue3/plugins/ or config/vue3/compilers/ needs '../../..'. A wrong root produces no import error: CONTEXT_ALIASES silently points at a non-existent vue3compat directory and the alias map resolves differently.

2. Non-recursive CI glob. .gitlab/ci/rules.gitlab-ci.yml:524, in .frontend-dependency-patterns, is "config/helpers/*.js", which does not match subdirectories. Moving these files out of config/helpers/ drops them from that pattern unless "config/vue3/**/*" is added. config/helpers/incremental_webpack_compiler/ is already invisible to it, and doc/development/pipelines/_index.md:173 documents the glob as recursive, which it is not.

Everything else already covers the new location: .assets-compilation-patterns:556-557 and .frontend-build-patterns:536 use recursive config/**/*.js and config/**/*.mjs, and scripts/lib/assets_sha.rb:15 uses config/**/*.{js,mjs}.

No CODEOWNERS change is needed. tooling/lib/tooling/validate_codeowners_coverage.rb checks top-level directories only, and config/vue3/ is second-level.

Scope of reference updates

Roughly 40 sites across 15 files. Grouped by how they fail:

  • Static imports, loud at require time: jest.config.base.js, vite.config.js, config/webpack.config.js, config/rspack.config.mjs, config/webpack.helpers.js, config/helpers/aliases.js, config/helpers/vite_plugin_page_entrypoints.mjs, config/rspack/vue.js, scripts/frontend/vue3_migration_stats.mjs.
  • String-built paths, quiet until runtime: scripts/frontend/infection_scanner/infection_scanner.mjs:19,91; config/webpack.config.js:200,206 and config/rspack/vue.js:50,55 (the vue-loader compiler option); config/helpers/vue3_infection_shared.js:107 (spawns the scanner by literal path).
  • jest.mock() strings, which fail as a silently un-mocked module: spec/frontend/config/webpack_helpers_spec.js:4,5 and spec/frontend/config/plugins/vue3_migration_manifest_plugin.spec.js:4,5.
  • Prose that goes stale: lib/gitlab/vue3_migration.rb:20,25,30; doc/development/fe_guide/vue3_migration.md:122,373,381,642; and five other comment sites.

Spec moves, and one trap

Specs move to mirror the new source paths. One of them is a trap: spec/frontend/scripts/infection_scanner/vue3_infection_shared_spec.mjs is an .mjs file, and Jest's moduleFileExtensions (jest.config.base.js:266) has no mjs. It runs only because vitest.infection_scanner.config.mjs:6 includes its directory. Moving it without widening that include makes ~50 tests silently stop running.

Since the vitest project would then cover more than the scanner, rename it for honesty: vitest.infection_scanner.config.mjs -> vitest.vue3.config.mjs, the package.json script vitest:infection-scanner -> vitest:vue3, and the CI job test-infection-scanner -> test-vue3-tooling.

Out of scope

  • lib/gitlab/vue3_migration.rb stays. Zeitwerk requires the path to match the constant Gitlab::Vue3Migration, and scripts/lint/keela_baseline.yml:249 is keyed by its literal path.
  • scripts/frontend/infection_scanner/ stays. It is an executable CLI with a co-located web UI, and its specs need vitest rather than jest.
  • app/assets/javascripts/lib/utils/vue3compat/ stays. It is application runtime, not tooling.
  • Renaming the "infection" concept. It is established across code, specs, docs and issue history, and renaming it would churn far more than the reorganisation. A glossary line in the new README covers it instead.

Prerequisites

Do not start until !252665 (merged) has merged. These MRs all touch the files this issue moves, so starting earlier would conflict with all of them at once:

MR state relationship
!252664 (closed) open adds the hand-listed CI paths this issue replaces
!252665 (merged) open hard prerequisite; touches config/helpers/context_aliases_shared.js
!252666 (merged) draft needs a rebase onto the new structure
!252667 (merged) draft needs a rebase onto the new structure

Verification

The strongest check is an invariant rather than a test. The scanner graph is keyed by absolute application paths and no config file appears in it, so moving config must leave tmp/infection_scanner.json byte-identical. That single assertion catches every __dirname depth mistake, because a wrong root changes CONTEXT_ALIASES and therefore how vue, vuex and friends resolve throughout the graph.

# on the merge base
node scripts/frontend/infection_scanner/infection_scanner.mjs
cp tmp/infection_scanner.json /tmp/graph_before.json

# after the move
node scripts/frontend/infection_scanner/infection_scanner.mjs
diff -q /tmp/graph_before.json tmp/infection_scanner.json   # must report no difference

# CE resolution is a genuinely different graph, so repeat
FOSS_ONLY=true node scripts/frontend/infection_scanner/infection_scanner.mjs

Then:

yarn vitest:vue3                    # incl. the moved .mjs spec
yarn jest spec/frontend/config      # catches broken jest.mock paths
yarn vite-prod                      # catches the vue-loader compiler paths
ENABLE_RSPACK=true yarn build       # INFECTION_LOADER_PATH changes value
bundle exec rspec spec/dot_gitlab_ci/rules_spec.rb \
                  spec/lib/gitlab/vue3_migration_files_spec.rb \
                  spec/helpers/webpack_helper_spec.rb \
                  spec/helpers/vite_helper_spec.rb

Checks that need eyes rather than a command:

  • test-vue3-tooling must appear on the MR's own pipeline. The MR touches config/vue3/**, so if the job is absent the new rule is wrong.
  • git log --follow should still trace history through the renames.
  • Grep rules.gitlab-ci.yml for config/vue3 and confirm .frontend-dependency-patterns picked it up.

Note spec/dot_gitlab_ci/rules_spec.rb:252-266 asserts every pattern in a *-patterns list matches at least one existing file, so a stale glob fails a spec rather than rotting quietly.

Novelty to flag in review

There is no precedent in this repo for relocating a group of config/ build files into a new directory. The closest is da73bb390d53 (scripts/frontend/compile_css.mjs -> scripts/frontend/lib/compile_css.mjs, 5 files, no CI or docs churn). The largest comparable rename is 2e65dd921b3a (config/dependency_cruiser.js -> .mjs, 13 files, touching CODEOWNERS, CI rules, eslint config, lefthook, package.json and 3 docs pages).

References

Edited by 🤖 GitLab Bot 🤖