Draft: Drop Vue 2 support from @gitlab/ui

⚠️ Disclaimer: 100% AI-generated

Every line of code, test, and this description was written by Claude (Opus 5.5) in Claude Code, directed and supervised by @xanf, with no hand-written code. Correctness claims rest on machine-checked evidence (the native Vue 3 jest run, pixel comparison against Vue 2 references, the Cypress suite, dist checks), not on human review. This MR targets the bootstrap-vue template rewrite !6550, so the diff only shows the Vue 3 switch.

What this does

packages/gitlab-ui itself becomes Vue-3-only: the published @gitlab/ui is built from it for Vue 3 and runs on native Vue 3 and on GitLab's @vue/compat runtime. There is no second package and no second package name. It replaces the separate-package approach of !6551 with the same code, applied in place.

Every component sets compatConfig: COMPAT_CONFIG (MODE: 3, plus the compat features that change what the component itself observes turned off), so it behaves on @vue/compat as on native Vue 3. v-model keeps working for @vue/compat consumers through the components' Vue 2 model option (COMPONENT_V_MODEL).

Commits

  1. Run packages/gitlab-ui on Vue 3 only (tooling)
    • Vue 3 is installed as the @gitlab/vue3 alias (npm:vue@3.5.32): vue in this monorepo is pinned to Vue 2 by the root resolutions for the Nuxt 2 documentation site. Jest maps vue to it, Storybook resolves its vue/dist/vue.esm-bundler.js to it, the build keeps vue external.
    • Vue Test Utils 2 (@vue/test-utils), @vue/vue3-jest, rollup-plugin-vue@6. The vue peer dependency is ^3.5.0.
    • Removed: Vue 2, vue-template-compiler, @vue/compat and the patched @gitlab/hybrid-vue (with its 19 patches and the patch update script), portal-vue, vue-runtime-helpers, Vue Test Utils 1, @vue/vue2-jest, vue-router 3, the Vue 2 Storybook framework, vue-loader 15, merge-cobertura, use_vue3.js.
    • Jest: one native lane. SFCs are compiled like the build (whitespace: 'preserve', no comments). vue-test-utils-compat keeps the Vue Test Utils 1 API the specs use, and runtime-compiled spec templates get v-model mapped to the model option, as @vue/compat does for GitLab.
    • Build: same per-file ESM dist, same entry points, same CSS output. yarn check:dist (new) checks the syntax level, leftover .vue/style imports, and that every name read from vue is a Vue 3 export (webpack and Rspack check export presence).
    • Storybook on the Vue 3 framework. Story templates are compiled with preserved whitespace, like the library's.
    • ESLint: the Vue 3 portability guard is removed.
    • The documentation site (Nuxt 2, Vue 2) depends on npm:@gitlab/ui@138.7.1 from the registry: it keeps showing the last Vue 2 release until it moves off Nuxt 2.
    • A major changeset and the FAQ entry describe the consumer changes below.
  2. Collapse the Vue version shim to its Vue 3 branch: vue_utils.js, root_events.js and vue_teleport.js keep their helper API, so components need no change. Only the Vue 3 branches stay, including the handling of @vue/compat consumers (.native listeners, the $root event bus, the own-event bridge, the vModelDynamic backfill). The Vue 2 Teleport and the Vue 2 SlotOutlet are deleted. SlotOutlet stays as the Vue 3 component: each remaining use renders a slot function of another instance, which a template cannot do without it.
  3. Apply the Vue 3 codemods to the gitlab-ui sources: lifecycle renames, $scopedSlots → $slots, Vue 3 directive hooks, component v-model desugaring in the library templates, functional → stateful, portal-vue → Teleport, compatConfig pins (bin/apply_compat_config.mjs, checked by yarn lint:compat-config).
  4. Make the gitlab-ui components and stories native Vue 3: listener, attribute, class and style forwarding through the shim helpers, null instead of false bindings, ARRAY_WATCH_OPTIONS, setReactive, the Vue 3 transition prop names in GlCollapse, base_dropdown.spec.js on Teleport. Stories: component v-model desugared, global registrations through Storybook's setup(), plain elements as portal targets.
  5. Run the gitlab-ui CI jobs on Vue 3 only: one lane for unit tests, the test Storybook, visual and integration tests (the vue2/vue3/hybrid duplicates and the coverage merge job are gone). ui:build_package also runs yarn check:dist.
  6. Filter fallthrough listeners by key in GlButton and GlForm: getNonFallthroughListeners and GlForm's listeners() leave a listener to the fallthrough when its key is in $attrs, not when the handler is identical. With consumer templates compiled by the Vue 3 compiler, a skipped child update replaces the vnode with a new handler array for the same event, and the identity check bound the listener a second time (Enter ran the handler twice).

Evidence

Local, on the branch head:

Check Result
Jest (native Vue 3, the only lane) 202/202 suites, 6254 passed, 32 skipped (the isNativeVue3 gates of @vue/compat-only cases)
yarn build 249 dist files: the 248 files of the Vue 2 build of the same commit plus utils/compat_config.js; index.css and tailwind.css byte-identical
Exported names identical in all 248 common dist files (export * resolved)
yarn check:dist 249 files parse as ES2019 modules (the level the Vue 2 dist already had), read only Vue 3 exports, no .vue/style imports
Visual (test-storybook, Firefox) 491/491 stories match the Vue 2 references rendered in the same environment
Cypress (Chrome) 77/77
ESLint, Prettier, markdownlint, yarn-deduplicate clean
Site unit tests 199/199, yarn build OK (on the 138.7.1 release)

CI: pipeline 2978864170 (merged results) passed:

Job Result
ui:unit_tests 202/202 suites, 6254 passed, 32 skipped
ui:build_package check:dist OK, tarball with the same 780 files as the Vue 2 package plus src/dist utils/compat_config.js
ui:visual 1/2, 2/2 491 stories match the committed references (no reference regenerated)
ui:integration_tests (cy:run, cy:edge) passed
lint, ui:lint, untamper-my-lockfile, site:build, site:unit_tests, site:e2e_dev, site:e2e_prod passed
ui:duo_job failed, allowed (duo-ui-allowed-to-fail label): duo-ui still runs Vue 2 and its own compat harness, which a Vue-3-only @gitlab/ui does not support

Tarball for consumers: https://gitlab.com/gitlab-org/gitlab-services/design.gitlab.com/-/jobs/17400220445/artifacts/raw/packages/gitlab-ui/gitlab-ui.xanf-gitlab-ui-vue3-in-place.tgz

What consumers must change

  • Vue 2 apps stay on the 138 major version.
  • Native Vue 3: bind the component's model prop and event instead of v-model (:value + @input unless the component's model option says otherwise, for example :selected + @select on GlCollapsibleListbox). @vue/compat keeps v-model working.
  • Code that reads $listeners or $scopedSlots of a library component, or emits open/close on GlTooltip and GlPopover, only works on @vue/compat with those features enabled for the caller: use $attrs, $slots and the methods. .native listeners only work on @vue/compat.
  • Directives define the Vue 3 hooks only. Code that calls a hook directly (GlResizeObserverDirective.bind(...)) calls beforeMount/unmounted instead. GlTooltipDirective also keeps bind, componentUpdated and unbind.
  • Specs that stubbed MountingPortal/PortalTarget stub teleport. The filtered search token segment still registers its teleport as Portal.
  • duo-ui has to move to Vue 3 before it can take this package.

🤖 Generated with Claude Code

Edited by Illya Klymov

Merge request reports

Loading
Loading