This post aims to document my experience migrating my Meteor 3.2 app from Cordova to Capacitor. It’s still a work in progress because I haven’t released the app to the app stores yet; I need to get in-app updates working (which I’ll talk about below). But for the most part, it seems very promising.
@nachocodoner’s post in this forum thread was the basis for me (and Claude) figuring out how to make this happen.
Update: new Capacitor plugin for Meteor
I made a plugin that ports cordova-plugin-meteor-webapp
to Capacitor (iOS only right now, Android to follow) and simplifies the build process while adding hot-code-push support and better backward compatibility.
A full set of instructions for how to get Capacitor up and running in a Meteor project (using that plugin) are available in the README linked above.
Initial method for getting Capacitor working
NOTE: The original instructions I posted here do not apply when using the @banj
Expand to see original instructions
Note, these steps are, to some extent, written from memory. I may be missing steps; let me know if I missed any important ones.
First, I added Capacitor to the projects per the docs:
meteor npm i @capacitor/core
meteor npm i -D @capacitor/cli
npx cap init
You will be asked some questions about how to set things up. Note, I chose to put all Capacitor-related files in the capacitor
directory, with capacitor/www-dist
as the directory that the Meteor-built files would be copied into, and capacitor/(ios|android)
as the directory where the Xcode/Android projects would be built and checked into git. Here’s the basic capacitor.config.ts
config to set this up:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.myapp.id',
appName: 'My App Name',
webDir: 'capacitor/www-dist',
ios: { path: `capacitor/ios`, },
android: { path: `capacitor/android`, }
};
export default config;
Then it’s time to set up the native project(s):
npx cap add ios
WebAppLocalServer shim
I put this in capacitor/WALS-shim.html
for use later on in the build script:
<script>
// WebAppLocalServer compatibility shim (replaces cordova-plugin-meteor-webapp)
if (typeof WebAppLocalServer === 'undefined') {
window.WebAppLocalServer = {
isShim: true,
startupDidComplete: function(cb) { cb && cb(); },
checkForUpdates: function(cb) { cb && cb(); },
onNewVersionReady: function() { /* noop */ },
switchToPendingVersion: function(cb) { cb && cb(); },
onError: function(cb) { /* noop */ },
localFileSystemUrl: function() { return 'INVALID_LOCAL_FILESYSTEM'; }
};
}
</script>
Build script
Now for the (first) tricky part: getting the Meteor client assets into the capacitor/www-dist
directory. Again, please read @nachocodoner’s post linked above to get the higher-level overview of what’s we’re doing here and what’s required.
Then, check out the script below, which automates this process. Note, I have this in my Justfile
with double-bracketed variables like {{root_url}}
; you would have to replace these with your values as needed if you’re not using just.
Note, this script is specific to iOS (and Mac’s version of sed
for that matter), but it could be adapted to work with Android as well.
#!/usr/bin/env sh
set -euxo pipefail
cleanup() {
kill $SERVER_PID 2>/dev/null || true
}
trap cleanup SIGINT SIGTERM # cleanup on Ctrl+C
export PORT=3333
export ROOT_URL='{{root_url}}'
# Version number for mobile app build - this is particular to the way I do version numbers on mobile
# We need something here because we're no longer using mobile-config.js
export MOBILE_VERSION="1.$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version.replace(/^(\d+\.\d+).*/, '\$1')")"
# Need a valid MongoDB connection to boot server;
# I grab this from SOPS-encrypted file in repo but you could theoretically run a local Mongo server too
export MONGO_URL=$(sops exec-env .deployment/secrets-qa.sops.env 'echo $MONGO_URL')
rm -rf {{build_dir}}
# Assumes public Meteor settings for mobile are at .deployment/mobile-settings.json
meteor build --directory {{build_dir}}/ --headless --server-only --mobile-settings ".deployment/mobile-settings.json" --server $ROOT_URL
rm -rf capacitor/www-dist || exit 1
mkdir -p capacitor/www-dist
cp -r {{build_dir}}/bundle/programs/web.cordova/*.js capacitor/www-dist || exit 1
cp -r {{build_dir}}/bundle/programs/web.cordova/app/* capacitor/www-dist/ || exit 1
rm -rf capacitor/www-dist/**/*.map || exit 1 # remove sourcemaps
(cd {{build_dir}}/bundle/programs/server && npm install --no-audit --loglevel error --omit=dev)
# Start server in background and capture PID
node {{build_dir}}/bundle/main.js &
export SERVER_PID=$!
sleep 3
# Fetch index.html and patch cordova paths
curl -f --connect-timeout 20 'http://localhost:3333/__cordova/index.html' | \
sed -e 's|/__cordova/client/|/client/|g' -e 's|/__cordova/|/|g' \
> capacitor/www-dist/index.html
# Inject WebAppLocalServer shim script
sed -i '' "3r capacitor/WALS-shim.html" capacitor/www-dist/index.html
# Sync www files to Capacitor
npx cap sync
# Sync version number and bump build number
(cd capacitor/ios/App/App.xcodeproj/; \
sed -i '' "s/MARKETING_VERSION = [^;]*;/MARKETING_VERSION = $MOBILE_VERSION;/g" project.pbxproj; \
awk '/CURRENT_PROJECT_VERSION = / { match($0, /[0-9]+/); gsub(/[0-9]+/, substr($0, RSTART, RLENGTH)+1) } 1' project.pbxproj > temp && mv temp project.pbxproj; \
)
# Open Xcode
npx cap open ios
cleanup
At this point, the project should open in Xcode and run successfully.
Cordova plugins
You’ll need to add Cordova plugins you depend on to your package.json
, but they should “just work”. I ended up switching to official Capacitor plugins in most cases, but I didn’t bother migrating my custom Cordova plugins.
Hot Code Push
I initially looked into @nachocodoner recommendation of using Capgo for this. Seems like a good service, but I preferred the idea of sticking with a “Meteor-native” approach of downloading assets and swapping them out, which is why I ported cordova-plugin-meteor-webapp
to Capacitor (with a lot of help from Claude). It leverages Capacitor’s native setServerBasePath()
method instead of running a custom HTTP server on iOS, so it’s simpler. I kept the download logic, state management, and recovery mechanisms from the original Cordova plugin (including many files with no changes at all) while using Capacitor’s built-in web serving capabilities for atomic bundle switching.
Android support
Haven’t even touched the Android side of things, but there’s no reason to believe that the Android part of cordova-plugin-meteor-webapp
couldn’t also be ported over to Capacitor. PRs welcome! I suggest we try to reuse as much of the old code as possible (as I’ve tried to do with iOS) so that we’re not reinventing the wheel and potentially introducing new bugs.
What about dev mode?
Haven’t figured out a good workflow for testing in development. I mean, I can set ROOT_URL
to a local web server and it will connect, but to update the code I have to run the whole build script again, which takes about 90 seconds on my M4 MacBook Air.
Then again, I’m using meteor-vite
and it’s never worked in dev mode with Cordova. If I switch back to Meteor bundling (i.e. with Rspack) someday, maybe I’ll look into this more. I suspect it would require changes to the Meteor core… not sure though.
I hope this might be a step in helping us Meteor devs to be able to use Capacitor in as smooth a way as possible, and perhaps getting Capacitor support in the Meteor core someday.
Let me know if you try this or have any ideas for how to improve things.