Port gitlab-ui to vue3
What does this MR do?
gitlab-ui-vue3: a native Vue 3 rewrite of @gitlab/ui, generated and verified by AI
⚠️ Disclaimer: 100% AI-generatedEvery 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
- 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 (
ctxpattern), 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. - Vue 2 → Vue 3 behavioral deltas are finite and enumerable. Everything the
four layers caught reduces to a short list (documented in
PORTING.md) - A native library must defend itself against
@vue/compatconsumers. A consumer's globalconfigureCompat({ MODE: 2 })broke 185/994 parity scenarios until every component declaredcompatConfig: { MODE: 3, COMPONENT_V_MODEL: true }and the Vue 2model:options were restored (inert in Vue 3, load-bearing under compat). Notably, bareMODE: 3— the "obvious" hardening — silently breaks consumerv-modelinstead. Now 994/994 under both compat modes, guarded byyarn test:compatin CI. - 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). - 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/uito 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/compatrenamingonXxxNativeover consumer listeners. Five genuine component bugs surfaced — textarea native-listener merge, toaster stub guard,v-gl-modalroot-bus contract, table items deep-watch, render-time slot enumeration — each fixed in this MR with a package regression test. - Real browsers against the prebuilt package surface a failure class jest
cannot see. The system-spec lane (label
pipeline:run-rspec-vue3on 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 globalprovide()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.nativeat compile time, so Vue 2 idioms like@click.capture.native.stopon dropdown items andv-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 — atransform-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 permanenttest:compatregression 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
