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.jsandvue3_infection_shared.jsboth say "shared" without saying shared between what. Word order flips between bundlers:vite_plugin_vue3_infection.mjsagainstwebpack_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.jsFiles 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:3path.resolve(__dirname, '../..')vue3_infection_shared.js:6path.resolve(__dirname, '..', '..')vue3_migration_loader.js:12path.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,206andconfig/rspack/vue.js:50,55(thevue-loadercompileroption);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,5andspec/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.rbstays. Zeitwerk requires the path to match the constantGitlab::Vue3Migration, andscripts/lint/keela_baseline.yml:249is 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.mjsThen:
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.rbChecks that need eyes rather than a command:
test-vue3-toolingmust appear on the MR's own pipeline. The MR touchesconfig/vue3/**, so if the job is absent the new rule is wrong.git log --followshould still trace history through the renames.- Grep
rules.gitlab-ci.ymlforconfig/vue3and confirm.frontend-dependency-patternspicked 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
- Parent problem this tooling exists to solve: #625296 (closed)
- Root cause analysis: #625296 (comment 3760799545)
- The MR whose hand-listed paths prompted this: !252664 (closed)
- Hard prerequisite: !252665 (merged)
- Needs rebasing afterwards: !252666 (merged) and !252667 (merged)