Skip to content

Migrate to Vue 3 with Quasar v2 and vee-validate v4#348

Open
claude[bot] wants to merge 28 commits into
developfrom
vue3-migration
Open

Migrate to Vue 3 with Quasar v2 and vee-validate v4#348
claude[bot] wants to merge 28 commits into
developfrom
vue3-migration

Conversation

@claude

@claude claude Bot commented Jul 9, 2026

Copy link
Copy Markdown

What

The complete Vue 3 migration: Vue 3 + Quasar 2 + Vuex 4 + Vue Router 4 + vee-validate v4, with @vue/compat fully removed and SSR re-enabled on the @quasar/app-webpack v3 architecture.

Replaces #347 (same commits) — recreated so the long-lived migration branch has a proper name (vue3-migration) and targets develop per Git Flow.

Highlights

  • All Vue 2 patterns eliminated (slots, $set/$delete, filters, event bus, render functions, is= usage); app runs on plain vue@3
  • Quasar v2 API adopted (model-value events/props, QTable rows, createMetaMixin — restores SSR meta/OG tags that had been silently broken)
  • SSR rewritten: middlewares (render/compression/api), production-export, hydration fixes
  • origin/main 2.42.0 merged in, with its Vue 2 feature code re-expressed in Vue 3
  • Migration guide for parallel Vue 2 branches: VUE3_MIGRATION_GUIDE.md
  • Verification: SPA + SSR builds green, SonarCloud quality gate passing, staging Docker image builds, headless-browser audits on 13 routes

Testing notes

Auth-gated screens (DM screen, run encounter, character builder) need manual staging verification — automated audits only cover public routes.

🤖 Generated with Claude Code

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ


Generated by Claude Code

HarmlessHarm and others added 28 commits February 22, 2026 16:52
Thorough analysis of the Vue 2 codebase (359 components, 17 mixins,
17 Vuex modules) with a phased migration strategy using @vue/compat.

Phase 0: Pre-migration on Vue 2 (remove filters, event bus, vue-cookies)
Phase 1: Core dependency swap (Vue 3 + compat, Quasar v2, Vuex 4, Router 4)
Phase 2: Stabilize dev build (fix Quasar v1->v2 breaks, re-enable plugins)
Phase 3: Incremental migration (Composition API, composables, remove compat)

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- Replace all 27 template filter pipes (| numeral) with formatNumber() method calls
- Create src/utils/formatNumber.js utility wrapping numeral.js directly
- Remove vue-numeral-filter plugin from boot/plugins.js and package.json
- Remove unused capitalize filter from contribute/spell/index.vue
- Replace Vue event bus (new Vue()) with mitt in src/event-bus.js
- Update 5 consumer files to use mitt API (emit/on/off instead of $emit/$on/$off)
- Remove vue-cookies (already fully migrated to Quasar Cookies plugin)
- Add mitt and numeral as direct dependencies
- SSR build verified passing

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
Upgrade core framework to Vue 3 with @vue/compat compatibility layer:
- vue 2.7 → 3.3, quasar 1.x → 2.x, vuex 3 → 4, vue-router 4
- @quasar/app → @quasar/app-webpack v3
- Rewrite quasar.conf.js → quasar.config.js (Quasar v2 format)
- Add @vue/compat configureCompat({ MODE: 2 }) boot file
- Rewrite store (createStore) and router (createRouter) for Vue 3 APIs
- Update all boot files: Vue.use()/Vue.component() → app.use()/app.component()
- Disable incompatible plugins: vee-validate, vue-shortkey, vue-snotify
- Remove vuefire (unused), vue-croppa (disabled), vue2-gtm (disabled)
- Fix <template v-for> key placement for Vue 3 compiler (22 files)
- Replace process.browser with typeof window !== 'undefined' (6 files)
- Remove dotenv from client bundle (patreon.js)
- Update ESLint: babel-eslint → @babel/eslint-parser, vue3-essential
- SPA dev server boots successfully with compat deprecation warnings

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
When no .env.{NODE_ENV}.local file exists, dotenv returns undefined
for parsed, leaving webpack's DefinePlugin with no replacements.
webpack 5 doesn't provide a global process object, causing
"ReferenceError: process is not defined" in firebase.js.

Now falls back to .env.dist when the environment-specific file is missing.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
Comprehensive plan covering 7 ordered steps to stabilize the dev build:
- Step 1: Fix .sync, ::v-deep, beforeDestroy, .native, $scopedSlots (59 files)
- Step 2: Replace vue-snotify with Quasar Notify + Dialog (46 files)
- Step 3: Replace vue-shortkey with custom Vue 3 directive (14 files)
- Step 4: Upgrade splitpanes, vuedraggable, verify flicking/vue-qr
- Step 5: Replace vue-croppa with vue-advanced-cropper (1 file)
- Step 6: Migrate vee-validate v3 to v4 (51 files)
- Step 7: Re-add Google Tag Manager (Vue 3 version)

Based on full codebase audit: 73 snotify calls, 24 keyboard shortcuts,
51 vee-validate consumers, 41 v-deep occurrences, 12 confirm dialogs.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
Replace deprecated star routes with the Vue Router 4 param syntax:
- /encounters/* → /encounters/:pathMatch(.*)*
- * → /:pathMatch(.*)*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace .sync modifier with v-model: on q-table (6 files)
- Replace ::v-deep with :deep() CSS combinator (38 files)
- Replace beforeDestroy → beforeUnmount (6 files)
- Replace destroyed → unmounted (2 files)
- Remove .native event modifier from hk-popover (1 file)
- Replace $scopedSlots → $slots (3 files)

51 files changed total.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
…f/$emit

- Replace all Vue.filter() usage with direct method calls and imported utilities
- Replace Vue.set/$set with direct property assignment (reactive in Vue 3)
- Replace Vue.delete/$delete with delete operator
- Replace $on/$off/$emit event bus patterns with mitt event emitter
- Update 46 files across components, views, and mixins
- Net reduction of 512 lines by removing Vue 2 boilerplate

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
… and component updates

- Replace Vue.set/Vue.delete with direct property assignment across all components and stores
- Migrate vee-validate from v3 to v4 with composition API setup
- Replace vue-snotify with Quasar Notify plugin for notifications
- Add custom v-shortkey directive to replace vue-shortkey plugin
- Update package.json dependencies for Vue 3 compatibility
- Remove deprecated snotify CSS styles
- Update store modules to use reactive patterns instead of Vue.set/Vue.delete

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
…t/$delete

- Migrate handleSubmit(callback) → handleSubmit($event, callback) for v4 in 13 files
- Fix { validate, valid } slot → { meta, validate } + meta.valid in 3 files
- Replace remaining errors[0] → errorMessage in ReminderForm.vue
- Remove all remaining $set/$delete calls across 23 files (124 occurrences)
- Update planning docs to mark Phase 2 as complete

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- Fix smart quote characters in confirmAction messages (Spells.vue, Monsters.vue)
- Restore missing if-condition and fix brace mismatch in RunEncounter.vue watcher
- Fix broken .then() chain in Profile.vue addVoucher method
- Fix &:deep(.pane) SCSS nesting that Sass can't suffix in RunCampaign.vue
- Run npm install to resolve missing declared dependencies

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- Fix dotenv fallback in quasar.config.js (parsed:{} is truthy on missing
  file, so .env.dist was never loaded and the app failed to boot)
- Default optional env keys (LOCAL_CHARACTER_SYNC_ID, MONSTER_GENERATOR_*)
  so webpack always replaces process.env references
- Guard Cookies.parseSSR behind process.env.SERVER in App.vue preFetch
- Rename modelValue computed to inputModel in hk-input/hk-select (collided
  with the prop of the same name)
- Fall back to a unique field name in hk-input/hk-select (vee-validate v4
  Field crashes on undefined name)
- Migrate leftover $root.$on/$emit to mitt EventBus (crumble breadcrumb
  + 5 compendium views)
- Replace Vue 2 @egjs/vue-flicking with @egjs/vue3-flicking and opt it out
  of compat mode
- Rename QTable :data to :rows (Quasar v2) in 18 occurrences
- Add .planning/VERIFICATION_REPORT.md with audit results, compat-debt
  inventory, manual smoke-test checklist and acceptance criteria

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- ssr.pwa is now gated on ctx.prod. In dev, Workbox's GenerateSW was
  emitting a new service-worker.js on every HMR poll; skipWaiting +
  clientsClaim then claimed all open clients and forced a refresh,
  which triggered another build, repeat. PWA only matters in prod
  anyway.
- mini-css-extract-plugin's 'Conflicting order' warnings are silenced
  via ignoreOrder: true. Every flagged module is a Vue scoped style
  (data-v-xxxxx attribute selectors), so cross-component specificity
  can't actually conflict and the warnings were pure noise.
No dependency version changes; npm 10.9.7 reshuffled 'peer' markers
while verifying the build.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- Convert all deprecated slot="name"/slot-scope attributes to v-slot
  syntax across 120 template files (382 + 47 occurrences); verified by
  eslint vue/no-deprecated-slot-attribute = 0 and headless-browser audit
- Remove all Vue.set/Vue.delete from 8 Vuex store modules (149 calls) —
  direct assignment/delete, splice for the one array case (state.log)
- Remove template $set/$delete usages (trackCampaign, campaign share,
  EditCampaign)
- Wrap async component registrations in defineAsyncComponent (boot file
  + 5 view/component files + Drawer.vue dynamic loader with markRaw)
- Rename transition classes to Vue 3 -from suffixes (GenerateMonster)
- Keep class/style on wrapper div in hk-input/hk-select (Vue 3 moves
  them into $attrs with inheritAttrs: false)
- Fix broken v-else-if on slot template in ContentSideRight (agent
  interruption casualty), styles tag typo in SpellCreator, stale
  ValidationProvider import in hk-action-roll-form
- Remove dead Vuex 3 store.js

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- Add Vue 3 v-model contract (modelValue prop + update:modelValue emit)
  alongside the Vue 2 contract to 16 custom components, so the
  COMPONENT_V_MODEL compat flag can be disabled without touching every
  consumer; display-only value-prop components identified and left as-is
- Add deep: true to the 3 array watchers that rely on in-place mutation
  (legacy Manual targeted, party xp/health allPlayers); other array
  watchers verified safe by reference replacement
- Replace Vue 2-only vue-qr with qrcode.vue + CSS logo overlay in
  PlayerLink (RENDER_FUNCTION compat source)
- Clear stale module declarations from typings (vue-shortkey, vue-qr,
  vue-numeral-filter all removed earlier)

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- Replace 36 Vue 2 render(c) router passthroughs with a shared
  { render: () => h(RouterView) } component (the last render-function
  compat dependency)
- Remove the vue → @vue/compat webpack alias, the compat boot file,
  and the @vue/compat dependency
- Drop the Flicking compatConfig opt-outs (no compat runtime to opt
  out of anymore)

Verified: production build succeeds; headless audit of 13 routes shows
zero Vue warnings and zero errors (only sandbox-environment network
noise). The app now runs on the standard vue@3 runtime.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
- Rewrite src-ssr for @quasar/app-webpack v3: middlewares (render,
  compression, api) + production-export.js replace the v1 extension.js/
  index.js; /api routes fail soft when firebaseServiceAccountKey.json
  is absent so SSR keeps working without server credentials
- Enable boot files server-side (component/directive registration and
  prototype extensions are needed during server render); GTM guarded to
  client, firebase-auth stays client-only
- Fix hydration mismatches: framework config dark: true (template
  hard-codes body--dark), SSR-stable useId() for vee-validate fallback
  field names, and invalid <thead>/<tr> HTML nesting in 7 table files
  (browser reparenting breaks hydration)
- Fix Vue 2-only is= usages: App.vue and character-descriptions to
  <component :is>, Initiative tbody to <transition-group tag="tbody">,
  campaign Players dynamic root restructured (named slots are dropped on
  dynamic plain elements in Vue 3)
- Restore Quasar Meta: v1 meta() option no longer works in v2; wrapped in
  createMetaMixin — SSR now renders per-route <title>, OG/Twitter tags
  and canonical links again
- Enable build.vueCompiler for runtime-compiled character descriptions

Verified: quasar dev -m ssr and quasar build -m ssr both work; the
production server renders real content with correct meta; SPA build
still passes; headless hydration audit shows no functional mismatches.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
Brings in NPC groups, campaign view refactor (#341), DM screen splitter
persistence (#339), Set Initiative NPC button (#344), and monster filter
changes (#345), re-expressed in Vue 3 syntax where main's code still
used Vue 2 patterns:

- slot="x" attributes -> v-slot templates (NpcGroupManager, BasicInfo,
  Npcs, Entities dialogs)
- $set/$delete/Vue.set/Vue.delete -> direct assignment (npcGroups and
  npcs store modules, hk-filter, Entities, BasicInfo)
- vue-snotify -> Quasar notify utils (NpcGroupManager, Entities, Npcs)
- vee-validate v3 slot props -> v4 errorMessage API (BasicInfo)
- QTable :data -> :rows; () => import() -> defineAsyncComponent
- process.browser -> typeof window guard in browserDetect (app failed
  to boot server- and client-side otherwise)
- adopted main's boot/prototypes.js (server-enabled for SSR)

Additionally fixes a latent Quasar v2 event-contract gap the merge
review surfaced: 101 @input handlers and 43 :value bindings on Quasar
form components renamed to @update:model-value / :model-value (Quasar
v2 components no longer emit input or read value; the handlers were
dead and several bindings one-way only).

Verified: SPA + SSR builds pass; headless audit of 10 routes renders
with zero Vue warnings.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
One-off dev script committed by accident during Phase 2; the migration
is complete and the script flagged 2 high-severity path-injection
findings in code scanning.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
Reproduced SonarCloud's BUG-type rules locally with eslint-plugin-sonarjs
over the full PR diff and fixed everything it found:

- hk-card/hk-tip: identical || operands (the $scopedSlots → $slots
  rename made both sides the same expression; deduplicated)
- trackCampaign/Meters: duplicate 'computed' option (merge artifact —
  JS silently discards the first block; npcSettings/allySettings merged
  into the single computed block so they work again)
- hk-table: explicit boolean returns in the search filter callback
- npcs store: explicit return in Promise.all map callback
- Entities: ternary used as statement → if/else

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
Web:InputWithoutLabelCheck — the sole Major bug behind the Sonar
Reliability gate. Adds id + associated label + aria-label to the
hidden file input in hk-image-uploader.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
npm 10 on Linux drops darwin-only optional deps from the lockfile when
mutating the tree (npm/cli#4828); npm 11 in the node:24 Docker image
then rejects the lock with 'Missing: fsevents@2.3.3'. Regenerated the
resolution with npm 11 so the staging image builds again. Verified
npm ci passes under both npm 10 and npm 11.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
hk-pane forwards its slots through q-scroll-area with
v-bind="scope". When a forwarded slot is invoked without props,
scope is undefined and guardReactiveProps(undefined) returns null,
so renderSlot reads null.key and the whole campaign DM screen dies
with 'Cannot read properties of null (reading key)'. Bind scope || {}
like hk-select already does.

Not caught by the route audits because hk-pane only renders on the
auth-gated Run Campaign / Run Encounter screens.

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
Documents the merge procedure, per-pattern conversion cheat sheet,
git conflict-resolution strategies, the mandatory post-merge pattern
sweep, and every runtime pitfall encountered during the migration and
the 2.42.0 merge (dead Quasar @input handlers, duplicate component
options, slot passthrough null scope, fsevents lockfile drops, SSR
table nesting, stale service workers, reading minified stack traces).

Also updates CLAUDE.md: tech stack now reflects Vue 3 / Quasar 2 /
Webpack 5, references the guide, and adds the post-migration
constraints (dual v-model contract, model-value events, npm 11
lockfile regeneration).

https://claude.ai/code/session_013pNZHMppjJ2iGVjxjHKkpQ
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants