Skip to content

Modernise the UI stack - #124

Merged
leoetlino merged 21 commits into
zeldamods:masterfrom
leoetlino:modernise
Oct 5, 2026
Merged

leoetlino merged 21 commits into
zeldamods:masterfrom
leoetlino:modernise

Conversation

@leoetlino

@leoetlino leoetlino commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

The app was built on Vue 2, Vue CLI 3 (webpack 4), TypeScript 3.9, BootstrapVue, Bootstrap 4 and Font Awesome 5. All of those are now end-of-life or unmaintained. We also needed a nasty workaround (--openssl-legacy-provider) just to make the build toolchain work with newer Node versions.

This set of commits migrates the app to Vue 3, Vite 8, TypeScript 7, Bootstrap 5, Font Awesome 7, pnpm while keeping everything in the UI (almost exactly) the same.

The Vue upgrade is the most tedious part because vue-class-component (which we were using for components) doesn't support Vue 3 so most of the changes to the objmap's own code are to migrate to the options API.

AI assisted, since the changes are mainly mechanical but extremely tedious and time consuming to do by hand. Recommend reviewing commit by commit. I've briefly tested this and couldn't find any regressions.


This change is Reviewable

@leoetlino
leoetlino requested a review from savage13 October 3, 2026 22:21
npm 12 refuses git dependencies by default, so a fresh install failed.
The fork only existed to render rails into a chosen pane, which upstream
already allows via L.Hotline.renderer({pane}). Updating the runtime
dependencies within their semver ranges picks up fixes and clears the
high-severity audit findings in shipped code; Vue, vue-router and the
newer @types packages are held back as they need Vue 2.7 or a newer
TypeScript. leaflet-path-transform's in-range 1.9 is a rewrite, but it
keeps the _transformPoints used to rotate Box areas, which still render
identically.

(AI assisted using Claude Opus 5.5)
Node 18 and the v3 actions are end-of-life; Node 26 is the newest LTS
line, supported until April 2029. Building every push and PR catches
broken builds before they reach master, and npm ci builds exactly what
the lockfile says. The rsync action moves to 9.0.1, pinned by commit
since it receives the deploy key; its inputs are unchanged.

(AI assisted using Claude Opus 5.5)
Vue 3 removes $on/$off, `new Component()`, prop mutation and .native.
A typed mitt bus, one helper that mounts the draw popup, local copies of
the popup's props and an explicit click emit from ObjectInfo let the
same call sites work on both versions, so the switch only has to rewrite
the mounting helper. Passing the native event keeps .stop/.prevent
working on ObjectInfo clicks.

(AI assisted using Claude Opus 5.5)
TS 3.9 can't parse current @types packages, and Vue 2.7 backports the
Composition API and Vue 3 typings as a stepping stone towards Vue 3.
Types are pinned to their last TS 4.9-compatible releases. Target drops
to es2020 because webpack 4's parser predates the class fields and
static blocks TS emits for esnext, and define semantics stay off for
vue-class-component and Leaflet subclasses. lintOnSave is off because
the old ESLint TS parser crashes under TS 4.9. vuedraggable has no Vue 3
release; vue-draggable-plus keeps its v-model + @update contract but
needs 2.7's Composition API, so the swap happens here.

(AI assisted using Claude Opus 5.5)
Vue 2 deep-walks any Leaflet layer put into component data, including
the whole L.Map; Vue 3 would proxy them instead, breaking Leaflet's
identity-based bookkeeping and the identity match that removes the
settings exclude sets. markRaw them, and the large lookup collections,
at creation; the component arrays holding them stay reactive. markRaw
also replaces the Unobservable wrapper, so the details pane now gets the
marker itself and re-clicking the open object no longer re-runs its init
(which drew a kept area a second time). Settings was only reactive
because AppMap put it in data(), which Vue 3 no longer observes in
place, so wrap it in reactive() explicitly; its Proxy still fires change
callbacks once per assignment.

(AI assisted using Claude Opus 5.5)
BootstrapVue has no Vue 3 support. Its components were mostly thin
wrappers adding Bootstrap classes, so native elements with the same
markup render the same. Bootstrap 4's own JS plugins need jQuery, so
until Bootstrap 5 a small local component handles the search preset
dropdowns, the only modal drives Bootstrap's markup itself, and hints
use native title tooltips. The Bootstrap 4 CSS stays, so the look is
otherwise unchanged.

(AI assisted using Claude Opus 5.5)
main.ts called Component.registerHooks() inside initUi(), after the
imports had already run the @component decorators, so beforeRouteUpdate
was only ever an uncalled method and hash navigation never moved the map.
Converting AppMap to an options object would silently turn it into a
live guard, and that guard aborts query-only navigations (?q=, ?id=)
because the map's moveend handler replaces the route mid-navigation.

(AI assisted using Claude Opus 5.5)
vue-class-component and vue-property-decorator have no Vue 3 release,
so they would block the switch. Objects the templates never read (the
map, controls, layer groups, markers, throttlers) become setup()
bindings so neither Vue version observes them; ui.late<T>() marks the
ones assigned after creation. ObjectInfo derives its object from its
props and drops cached metadata when it changes, so an instance reused
by an index-keyed v-for can't show a stale object. Emits are declared
so Vue 3 won't also bind listeners on the root as fallthrough attributes.

(AI assisted using Claude Opus 5.5)
These relied on Vue 2 and router 3 behaviour that changes on the switch.
Vue 3 only tracks writes through the proxy, so user exclude sets must be
reactive before init() fills their ids or the hidden count stays at 0.
The search link spreads $route.query, as router 4 ignores a query
embedded in a path, and router-link's append, a no-op here, is gone.

Unparsable map coordinates now fall back to the default view directly:
the old replace() to the same route was a duplicate navigation in both
routers, so no view was set and map init threw on getCenter().

(AI assisted using Claude Opus 5.5)
Vue CLI 3 is unmaintained, needs --openssl-legacy-provider on current
Node, and its webpack 4/Babel toolchain blocks further upgrades. Its
ESLint 5 stack goes with it, so linting is unavailable for now; Vite
does not type-check, so CI runs tsc first. Output now targets Vite's
default browser baseline, without Babel or autoprefixer, and the dev
server moves to Vite's port 5173.

leaflet-hotline is registered by hand because Vite gets its CommonJS
factory, not the self-registering AMD branch webpack took. Vite's bundle
is strict-mode ES, where leaflet-draw's assignments to undeclared
globals throw and break rectangle drawing and circle resizing, so those
globals are created up front. VUE_APP_ stays as the env prefix so existing
.env files keep working. public/game_files stays out of dist: it is
often a symlink to ~300 MB of game data that the app fetches from
VUE_APP_GAME_FILES anyway.

(AI assisted using Claude Opus 5.5)
Vue 2 is end-of-life, and the last blockers (BootstrapVue, class
components) are gone. Vue 3 mounts inside #app instead of replacing it,
so the container needs the full height and App.vue no longer owns the id;
beforeDestroy hooks are renamed because Vue 3 silently ignores them.
Router 4 navigates asynchronously, so mounting waits for the first
route that AppMap reads, and it resolves duplicate navigations instead
of rejecting them. Vue 3's whitespace condensing matches Vue CLI's
except between elements on one line, so the compat option goes and the
one such space is removed. Vue 3 tracks Set mutations, so the filter
button reads Settings.shownGroups directly instead of keeping a copy.

(AI assisted using Claude Opus 5.5)
Linting had been off since the ESLint 5 stack could not parse modern
TypeScript, and it left with Vue CLI. A standalone flat config with
typescript-eslint and eslint-plugin-vue's Vue 3 essential preset runs as
plain `eslint .`; it also picks up no-var, prefer-const and ESLint 10's
newer recommended rules such as no-useless-assignment. With their
findings fixed (var and never-reassigned let, dead initial values, a
no-op self-assignment) the tree lints clean, so CI can enforce it. The
vendored tile workaround is ignored rather than reformatted.

(AI assisted using Claude Opus 5.5)
Each of these outlived what it belonged to for the rest of the session:
draw popup apps were never unmounted when their layer was deleted, split
or replaced, every object view added another ColorScale:change map
listener, and Leaflet does not remove a rail's own hotline renderer with
the rail, leaving an empty canvas in the tile pane. Persistent rails get
a listener tied to the colour scale so they still recolour.

(AI assisted using Claude Opus 5.5)
TS 4.9 held the @types packages at old pinned versions. TS 7 type-checks
the project, but it has no compiler API yet, so typescript-eslint runs on
TS 6 installed as `typescript`, the side-by-side setup the TS team
recommends. The tsconfig still carried Vue CLI/Babel-era settings: Vite
resolves like a bundler and esbuild transpiles each file on its own, so
say so and drop the rest. With no Babel to appease, ES2022 class-field
semantics are safe: the only Leaflet subclass with a redeclared field
already uses `declare`. Newer TS rejects literals that aren't enum
members, so the 0 passed as a no-op mode gets a None member; it also
reports the draw event map where a @ts-ignore can't reach, so the map
gets a type instead. Newer @types/leaflet makes radius optional, so the
CanvasMarker options cast goes.

(AI assisted using Claude Opus 5.5)
Bootstrap 4 is end-of-life. v5 dropped or renamed several classes we use
(spacing, custom controls, form-group, btn-block, .close) and changed
enough defaults to be noticeable on our dark panes, so this also keeps
the v4 look where it matters. Without Popper, a menu is only anchored
below its toggle with data-bs-popper, so AppDropdown sets it as
Bootstrap's static dropdowns do. Toggled buttons sit in a d-grid since
display utilities are !important and would defeat v-show.

(AI assisted using Claude Opus 5.5)
The hand-rolled dropdown and modal only existed because Bootstrap 4's JS
needed jQuery. They lacked keyboard navigation, focus trapping and focus
return, and a drag from the modal's inputs onto the backdrop closed it.
Bootstrap 5's plugins are the reference behaviour for the markup we
render and need only Popper, which also brings back styled tooltips in
place of the native title ones.

(AI assisted using Claude Opus 5.5)
Font Awesome 5 is end-of-life. FA7 still ships the fa/fas/far prefixes
and the v5 icon names as aliases, so no template changes are needed.
FA7 makes every icon fixed-width by default, which widened inline and
tab icons and shifted buttons; resetting --fa-width keeps FA5's natural
widths while fa-fw still gives fixed width. Picked over FA6, whose font
metrics made search result rows showing the ⚔ glyph 1px taller.

(AI assisted using Claude Opus 5.5)
Vue Router 5 is the current line, and 4.x has had no release since
December 2025. v5 folds file-based routing into the core package with
no breaking changes for plain v4 usage; the build-time dependencies it
pulls in are not bundled, and the app bundle stays the same size.

(AI assisted using Claude Opus 5.5)
Immer 10 has had no release since October 2025. 11's one breaking
change, iterating only enumerable own properties by default, cannot
affect us: we only draft plain GeoJSON objects when splitting a drawn
line, which still works.

(AI assisted using Claude Opus 5.5)
Vite 8 bundles with Rolldown and transforms/minifies with Oxc and
Lightning CSS, which builds several times faster and drops esbuild and
Rollup from the dependency tree.

Rolldown's namespace import of a CJS module is a snapshot taken before
plugins run, so MapBase needs the default leaflet import to see
L.RasterCoords; without it the map never initialised. "type": "module"
keeps vite.config.ts compatible with the native config loader Vite
plans to make the default, which it otherwise warns about on every run.

The default build target rises to Chrome/Edge 111, Firefox 114 and
Safari 16.4, so Lightning CSS also drops some vendor prefixes.
Headless browser checks pass against both the build and the dev server.

(AI assisted using Claude Opus 5.5)
pnpm links packages from a shared store instead of copying them into
every checkout, and its strict node_modules layout makes an import of an
undeclared dependency fail instead of silently working. The lockfile is
imported from package-lock.json, so the resolved versions stay the same.
packageManager pins the pnpm version for local use and for CI, where the
setup action is pinned by commit like the deploy one.

(AI assisted using Claude Opus 5.5)

@savage13 savage13 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.
Wow. Nice work (lots of changes)

Updated version works on my machine.

I went through the PR "commit by commit" (in github) and "file by file" (in Reviewable)

cf1065c ok Upgrade to Vite 8
acf5b33 ok Upgrade immer to 11
984254b ok Upgrade vue-router to 5
a280517 ok Upgrade Font Awesome to 7
5cd0d92 ok Use Bootstrap's own dropdown, modal and tooltip JS
b5d8455 ok Upgrade to Bootstrap 5
css styling
ca5ec1c ok Upgrade TypeScript to 7 and modernise tsconfig
889a84c ok Fix draw popup, ColorScale listener and hotline renderer leaks
leaky rails
60461af ok Lint with ESLint 10 and a flat config, and lint in CI
af2352b ok Switch to Vue 3 and vue-router 4
more defineComponent/Component changes
beforeDestroy => beforeUnmount
cd546f5 ok Build with Vite instead of Vue CLI 3
remove openssl, update to Vite
ec77c5a ok Prepare routing and exclude sets for vue-router 4 and Vue 3
ec4286c ok Convert all components to defineComponent and drop decorator libraries
add defineComponents replacing decorators (cleaner)
big commit
0f457dc ok Drop AppMap's beforeRouteUpdate hook, which never ran
2ac3de1 ok Remove BootstrapVue
Add AppDropDown, BootstrapVue removed, b-tagname removed
e947897 ok Keep Leaflet objects out of Vue reactivity and make Settings reactive
markRaw, remove ui.Unobservable (remove .data access)
add reactive to settings, fantastic :)
cd77321 ok Upgrade to TypeScript 4.9 and Vue 2.7
VueDraggable added (vue-draggable-plus)
a8952c3 ok Replace Vue-2-only patterns ahead of the Vue 3 migration
Added AppMapPopupProps, MountedComponents, event hanlding modified
appMapBus created, AppMapPopup modified, mountComponent created
5a3f2d3 ok CI: build every push and PR on Node 26, deploy only from master
d6e282f ok Use leaflet-hotline from npm and update deps within their ranges

@savage13 partially reviewed 60 files and all commit messages, and made 1 comment.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on leoetlino).

@leoetlino
leoetlino merged commit 9a937fa into zeldamods:master Oct 5, 2026
2 checks passed
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.

2 participants