wekan:main β harryadel:meteor-3-migration
opened 11:55PM - 23 Mar 26 UTC
I had a few days left before my Claude and Codex subscriptions ran out, so I decβ¦ided to put what remained of them to good use. π
We could've attempted jumping straight to 3.0 and then to 3.4, but that would've only lengthened the pain β death by a thousand paper cuts. The application restructuring was by far the lengthiest and most annoying part of this whole migration journey, mostly because of the overuse of globals and the lack of separation between server and client responsibilities (the classic isomorphism trap). These steps could've technically been split into separate PRs, but here we are.
---
### Client Boot and Entry Points
We replaced the old eager client import graph with explicit feature entry points under `client/features`. Each feature now owns its template, logic, and style loading order instead of relying on Meteor 2's permissive global/eager behavior.
- `client/imports.js` now imports feature entry modules instead of a flat list of component files
- `client/styles.js` centralizes stylesheet loading
- `client/main.js` boots styles and client features in a controlled order
- Shared sidebar access moved behind `client/features/sidebar/service.js` to break circular imports
---
### Shared Model and Server Boundary Split
We split the old hybrid `models/*.js` design into a cleaner shared/server architecture.
The pattern introduced:
- Shared collection definitions remain in `models`
- Server-only methods, hooks, startup, indexes, routes, and side effects moved into `server/models`
- Permissions moved into `server/permissions`
- Server boot wiring consolidated in `server/imports.js`
---
### Removal of Implicit Globals
A major migration theme was eliminating Meteor's ambient-global assumptions. What we cleaned up:
- Explicit imports for `Template`, `Meteor`, `ReactiveVar`, `Session`, `ReactiveCache`, `EscapeActions`, `Utils`, `Swimlanes`, and `TAPi18n`
- Client route and popup code no longer assumes Blaze globals or that a current view context will always exist
- Model/schema code no longer treats `SimpleSchema` as an ambient global owned by `imports/collectionHelpers.js`
---
### Asyncification for Meteor 3
A large portion of the work involved converting server code away from Meteor 2's synchronous Mongo APIs:
- `findOne` β `findOneAsync`
- `count` β `countAsync`
- `fetch` β `fetchAsync`
- `insert` / `update` / `remove` β async variants where needed on the server
- Direct collection operations and hook bodies updated to properly `await` async writes
- Synchronous callback patterns like `forEach(async ...)` rewritten to `for...of` where required
---
### SimpleSchema & Collection2 Compatibility
- **SimpleSchema v2 migration** β switched from a named export to the default import, registered custom schema extensions (`denyUpdate`, `denyInsert`), and added missing parent object declarations in schemas.
- **Centralized `collection2`/`SimpleSchema` setup** β `imports/collectionHelpers.js` shim ensures Collection2 and SimpleSchema are loaded exactly once before any model file calls `.attachSchema()`.
---
### Synced Cron Dormant Bug
There was a dormant bug that manifested as a server crash with no clear error output β it looked like Mongo, SockJS, or DDP instability. The actual root cause was buried:
- `quave:synced-cron` was still in the boot path
- Unhandled rejections in cron-related code caused the package to exit the process
- The browser only showed the downstream effects: WebSocket closed, SockJS JSONP failed, and HTML error pages appeared where JS/JSON was expected
What we changed:
- Centralized cron access in `server/cron/syncedCron.js`
- Removed scattered startup ownership of cron behavior
- Changed the wrapper to lazy-load `quave:synced-cron` instead of importing it eagerly at normal app boot
- Replaced some automatic housekeeping behavior with simpler non-cron paths
- Fixed the real fatal unhandled rejection in `server/notifications/outgoing.js` that cron was surfacing
---
### The Markdown Package Problem
The app depends on `wekan-markdown`, which:
- Exports a global `Markdown`
- Wires client behavior through `api.addFiles('src/template-integration.js', 'client')`
- Relies on legacy package-loading semantics rather than explicit ES module imports
Under Meteor 2, this mostly worked because package/global loading was eager and forgiving. The tricky part was that this package sat in an awkward middle ground: not a normal app module, not a fully modernized Meteor package, and still relying on client integration happening as a side effect.
So when the client boot graph was being cleaned up, markdown appeared broken even though the package still existed and its name in `.meteor/packages` was still correct. The real problem was that package side effects could no longer be casually assumed to happen at the right time.
This was a huge learning moment. I had assumed that migrating a Meteor package was all about `versionsFrom` and converting synchronous code β but it's also about structure and the implicit guarantees Meteor 2 provided that simply no longer exist in the Meteor 3 world.
---
### Meteor Packages
- `packages/meteor-autosize/` β replaced with the npm `autosize` package
- `mquandalle:jade` β replaced with `meteor-jade-loader`, a custom rspack Jade loader
- `meteorhacks:subs-manager` β replaced with `jam:offline`, `jam:method`, and `jam:pub-sub`
- `simple:json-routes`, `simple:rest-accounts-password`, and `communitypackages:picker` β replaced with a custom implementation using the native WebHandler approach. I did actually migrate the `simple` packages [here](https://github.com/Meteor-Community-Packages/meteor-rest), but ultimately decided it was better for WeKan to drop them entirely. This change specifically is partially why I was itching to bump to 3.0 directly instead of doing mini-PRs as some changes can only be done when you've already upgraded to 3.0 and cannot be done on 2.0. Maybe the core team is ought to move the express change to 2.0 to ease this burden a bit.
---
### Removed NPM Packages
| Package | Notes |
|---|---|
| `flatted` | devDep |
| `@mapbox/node-pre-gyp` | |
| `ajv` | |
| `bcryptjs` | |
| `chart.js` | |
| `es6-promise` | |
| `fibers` | Core Meteor 2 dependency, not needed in Meteor 3 |
| `ldapjs` | |
| `os` | |
| `qs` | |
| `simpl-schema` | |
| `source-map-support` | |
| `to-buffer` | |
| `uuid` | |
### Added NPM Packages
| Package | Notes |
|---|---|
| `@meteorjs/rspack` | devDep β new Meteor 3 bundler |
| `@rsdoctor/rspack-plugin` | devDep |
| `@rspack/cli` + `@rspack/core` | devDep β rspack toolchain |
| `css-loader` + `style-loader` | devDep β rspack CSS pipeline |
| `@swc/helpers` | Runtime helpers for the SWC transpiler |
| `autosize` | Replaces the `meteor-autosize` Atmosphere package |
### Upgraded NPM Packages
| Package | Old | New |
|---|---|---|
| `@babel/runtime` | 7.28 | 7.29 |
| `bson` | 4.x | 7.x |
| `dompurify` | 3.3.2 | 3.3.3 |
| `filesize` | 8.x | 11.x |
| `hotkeys-js` | 3.x | 4.x |
| `i18next` | 21.x | 25.x |
| `limax` | 4.1.0 | 4.2.3 |
| `markdown-it` | 12.x | 14.x |
| `markdown-it-emoji` | 2.x | 3.x |
| `markdown-it-mathjax3` | 4.x | 5.x |
| `pretty-ms` | 7.x | 9.x |
| `sinon` | 21.0.1 | 21.0.3 |