Port gitlab-ui to vue3

What does this MR do?

image

gitlab-ui-vue3: a native Vue 3 rewrite of @gitlab/ui, generated and verified by AI

⚠️ Disclaimer: 100% AI-generated

Every line in this MR — source code, tests, CI configuration, documentation, and this description — was written by Claude (Fable 5) running in Claude Code/pi.dev/own created harness, directed and supervised by @xanf but with no hand-written code. Correctness claims below rest on machine-checked evidence (fixtures generated from the real Vue 2 implementation, verbatim upstream test suites, pixel comparison), not on human code review. Tread accordingly: the evidence pyramid is the primary reviewable artifact, and the 237k inserted lines is its output.

What this is

A proof-of-concept answer to: can @gitlab/ui be rewritten in native Vue 3 — no @vue/compat, no mixins, Composition API throughout — as a drop-in replacement? The packages/gitlab-ui-vue3 package ports the entire library (all components plus the vendored bootstrap-vue internals) in 135 commits, roughly one component per commit, topologically ordered.

This started as my learning on ultracode features of Claude Code and was greatly inspired by Bun in Rust blog post, but quickly moved more than just "hey, Claude code, ultracode this stuff"

"Drop-in" is a measured claim. Six independent layers hold the port to the Vue 2 original — the first four running in MR pipelines, the fifth and sixth on a companion branch of the monolith:

Layer Result
Unit suite (rstest, ported from jest) 4,908 / 4,908
Markup + event parity vs fixtures rendered by the real Vue 2 library 994 / 994 scenarios, byte-identical markup and identical event payloads and ordering
Upstream Cypress e2e suite, copied verbatim (zero spec edits) 76 / 76
Pixel comparison of the two Storybooks (jest-image-snapshot, firefox) 478 / 478
gitlab-org/gitlab's entire frontend jest suite, run against this package on branch xanf/gitlab-ui-vue3-embed 6,059 / 6,059 suites — 90,562 / 90,562 tests, 526 snapshots
gitlab-org/gitlab's real-browser system-spec suite (48 rspec system pg17 vue3 shards via the pipeline:run-rspec-vue3 label, package consumed prebuilt — webpack CJS dist, no source compilation by the host) 44 / 48 shards green (final run); the 4 residual spec files: 2 fail identically on the baseline compat lane (pre-existing debt), 1 under investigation (pipeline graph page, apollo returns empty data without error), 1 intermittent EE spec — effective parity with the baseline vue3 lane

Main findings

  1. The rewrite is mechanical once the mixin problem is solved. BSV's cross-referencing mixins become composables registering members on a shared per-instance context (ctx pattern), with watcher queueing reproducing Vue 2's init and flush order. Event-order parity fixtures were the key instrument — they caught scheduler differences no ordinary test would.
  2. Vue 2 → Vue 3 behavioral deltas are finite and enumerable. Everything the four layers caught reduces to a short list (documented in PORTING.md)
  3. A native library must defend itself against @vue/compat consumers. A consumer's global configureCompat({ MODE: 2 }) broke 185/994 parity scenarios until every component declared compatConfig: { MODE: 3, COMPONENT_V_MODEL: true } and the Vue 2 model: options were restored (inert in Vue 3, load-bearing under compat). Notably, bare MODE: 3 — the "obvious" hardening — silently breaks consumer v-model instead. Now 994/994 under both compat modes, guarded by yarn test:compat in CI.
  4. Visual baselines bind to their rendering environment, including CPU architecture. The unmodified Vue 2 storybook fails 20 of its own committed x86_64 baselines in an arm64 container of the identical browser image. The CI job therefore holds the Vue 3 storybook to the same committed baselines as the Vue 2 one, in the same image; local verification regenerates references per-machine (workflow in PORTING.md).
  5. The real consumer is the strongest layer: the monolith's whole jest suite passes against the port. On a companion branch (gitlab-org/gitlab@xanf/gitlab-ui-vue3-embed), an opt-in flag (VUE_VERSION=3 GITLAB_UI_VUE3=1) remaps @gitlab/ui to this package and the full suite went 92 → 61 → 25 → 0 failing suites (6,059 suites, 90,562 tests, 526 snapshots, zero failures; the 25 formerly-failing suites also stay green on the unflagged baseline). Most of the residue was test-harness physics rather than component behavior: the vue-test-utils compat shim upgrading wrappers past their v1 API (setSelected), stub factories probing Vue 3 function components as legacy async factories, Vue 2's compiler dropping template comments/edge whitespace (now emulated in a scoped jest transformer), and @vue/compat renaming onXxxNative over consumer listeners. Five genuine component bugs surfaced — textarea native-listener merge, toaster stub guard, v-gl-modal root-bus contract, table items deep-watch, render-time slot enumeration — each fixed in this MR with a package regression test.
  6. Real browsers against the prebuilt package surface a failure class jest cannot see. The system-spec lane (label pipeline:run-rspec-vue3 on gitlab!246445) started at 256 failing spec files and burned down 256 → 51 → 18 → 10 → 6 → 4 across six rounds, ten root causes, all package-side or wiring-side, none caught by 90k jest tests: @vue/compat resolves features against the component being created, so pins on OUR components changed how the consumer's global provide() mixins merged (feature flags silently wrong app-wide) and how consumer-supplied async component factories were converted (tokens rendering as the literal text [object Promise]); the Vue 3 compilers erase .native at compile time, so Vue 2 idioms like @click.capture.native.stop on dropdown items and v-on="$listeners" forwarding need deliberate, per-component contracts (blanket fixes double-deliver — the parity fixtures caught that); and prebuilt consumption has physics of its own — a transform-based portal wrapper flattens the dropdown z-index into a losing stacking context, a stateful config module reachable both from source and from the bundle splits into two registries (translations silently not applied), and a module-level modal registry broadcasts show-by-id across app boundaries where Vue 2's bus was per-$root. Every fix ships with a jsdom+compat reproduction against the production dist and, where consumer-contract-shaped, a permanent test:compat regression spec.

Responsible token consumption

Measured across all 57 session transcripts — including subagent transcripts, which hold ~75% of the output tokens — deduplicated per unique API request (4,118 requests, all on Fable 5).

Token category Tokens Rate / MTok Cost
Input (uncached) 42,747 $10.00 $0.43
Prompt-cache writes 37,992,543 $12.50 $474.91
Prompt-cache reads 970,507,968 $1.00 $970.51
Output 6,159,023 $50.00 $307.95
Total $1,753.79

(cost based on API prices so does not 100% reflect real consumption)

Per phase: PoC scaffold $28 · full library port $1,667 · visual regression + CI + compat hardening $59. Roughly $13 per ported component, evidence included. Notably, 55% of the spend is prompt-cache reads — the price of re-reading a large working context on every request — and only 18% is generated output.

This is one-time significant spent, with an intention to keep it updated, not thrown off Commit used as reference: 910fa478


Edited by Illya Klymov

Merge request reports

Loading
Loading