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
- 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. - The wave — codemod applied (141 files / 248 sites) + the single
initSimpleAppbare-factory site. - Jest harness + 4 specs — the VTU2 auto-stub factory stubbed
defineAsyncComponentwrappers as anonymousAsyncComponentWrapperstubs and never registered the stub for the loader's resolved component, sofindComponent(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 nowawait waitForPromises()first. - Infection scanner —
detectAppRootrequired a bare default vue import, so sixnew Vuebootstraps whose import clause gained, { defineAsyncComponent }lost their app-root firewall and infection leaked to 30 importers.defineAsyncComponentis 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 0clean on all touched files.
Out of scope (follow-ups)
- Component-map objects and dynamic
:isfactories (~14 files) — a semantically distinct class, each site needs judgment. - Files whose conversion additionally depends on the pending initVueApp stack (
render_json_table.jsand friends). - Route-level
component: () => import()stays untouched — Vue Router owns lazy route resolution anddefineAsyncComponentmust 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.