Wrap legacy async component factories in defineAsyncComponent

What does this MR do and why?

Converts every legacy async component factory in a components: option block — MyThing: () => import('...') — to defineAsyncComponent(() => import('...')), plus the one bare factory passed to initSimpleApp (141 files / 248 sites + 1).

Vue 2 auto-converts bare factories and @vue/compat emulates that, but plain Vue 3 treats a plain function component as a functional component, so the factory's returned import promise is not a vnode and the subtree renders nothing, silently. defineAsyncComponent is exported by vue@2.7, @vue/compat and vue@3, so the wrapped spelling is dual-runtime and behavior on both current lanes is unchanged. This is part of the Vue 3 migration extraction series (previously: !247132 (merged), !247312 (merged), !247298 (merged), !247299 (merged)) and is independent of the pending initVueApp stack — zero file overlap.

Commits

  1. Codemod (scripts/frontend/codemods/vue3_define_async_component.mjs) — rewrites components-option factories; kept committed like the other wave codemods so the MR is self-contained and re-runnable.
  2. The wave — codemod applied (141 files / 248 sites) + the single initSimpleApp bare-factory site.
  3. Jest harness + 4 specs — the VTU2 auto-stub factory stubbed defineAsyncComponent wrappers as anonymous AsyncComponentWrapper stubs and never registered the stub for the loader's resolved component, so findComponent(TheImportedDefinition) matched nothing (bare factories got both behaviors for free). Async-wrapper stubs are now named after their registration key and register for the resolved component, mirroring the legacy-factory branch. Four specs on the Vue 2 lane assert on an async child synchronously and now await waitForPromises() first.
  4. Infection scannerdetectAppRoot required a bare default vue import, so six new Vue bootstraps whose import clause gained , { defineAsyncComponent } lost their app-root firewall and infection leaked to 30 importers. defineAsyncComponent is now stripped from vue import clauses before the named-import veto (any other named import still disqualifies). Scanner baseline is back at exact master parity: 546 app roots, 5,821 infected files.

Verification

  • 125 covering jest suites green on both lanes (default and VUE_VERSION=3): 171/171 suites, 4,185 passed / 11 skipped each.
  • Every failure observed during development was triaged against a clean master baseline (isolated re-runs on both trees) — all were the async-timing/stubbing classes fixed in commits 3–4, no order-dependent flakes swept in.
  • Infection-scanner vitest suite 111/111; full scanner run byte-parity with master.
  • eslint --max-warnings 0 clean on all touched files.

Out of scope (follow-ups)

  • Component-map objects and dynamic :is factories (~14 files) — a semantically distinct class, each site needs judgment.
  • Files whose conversion additionally depends on the pending initVueApp stack (render_json_table.js and friends).
  • Route-level component: () => import() stays untouched — Vue Router owns lazy route resolution and defineAsyncComponent must not wrap route components.

Note for the initVueApp stack: analyze.mjs#detectAppRoot is also touched by !247298 (merged); whichever lands second has a trivial conflict in that function.

🤖 Generated with Claude Code

Edited by Illya Klymov

Merge request reports

Loading
Loading