@a4xrbj1/meteor-desktop
v6.0.18
Published
Build a Meteor's desktop client with hot code push.
Maintainers
Readme

Meteor Desktop
aka Meteor Electron Desktop Client
Build desktop apps with Meteor & Electron. Full integration with Meteor's hot code push for web content.

What is this?
This is a complete implementation of integration between Meteor and Electron aiming to achieve the same level of developer experience like Meteor gives.
To make it clear from the start, this is a desktop client - it is just like your mobile
clients with Cordova - but this is for desktops with Electron. It also features a full hot code
push implementation - which means you can release updates the same way you are used to.
Originally authored by Paweł Wójtkowiak. Currently maintained by @a4xrbj1.
What's new in v5
v5.0.0 is a major release focused on Meteor 3.x compatibility, ESM support, and a significantly leaner, more reliable build pipeline.
Key highlights:
- Meteor 3.x support — switched from
web.cordovatoweb.browserarchitecture;/__browser/paths,isCordovadetection, and autoupdate all updated for Meteor 3.x. - ESM / Electron 33+ compatibility — replaced deprecated
registerStreamProtocolwithprotocol.handle; patched dev-server responses forimport.meta,global, and classic-script constraints. - HCP improvements —
cordova.jsrenamed todesktop-hcp.js; auto-injected intoindex.html; fail-fast guardrails added to autoupdate. - Dependency debloat — removed
node-fetch,shelljs,lodash,rimraf,isbinaryfile, anddel; all replaced with native Node.js APIs. Eliminated thedist/build step. - Hardened build pipeline — seven new validation gates (A1–A7) catch broken bundles early; every error path now throws or exits instead of silently continuing.
- Reliability —
fs.rmSyncrace-condition fixes for macOS;chmodRecursiveruns on all platforms; 13+ silent error paths surfaced.
See CHANGELOG.md for the full list of changes.
Prerequisites
- Meteor >=
3.0 - at least basic Electron framework knowledge
- mobile platform added to project*1
*1 you can always build with --server-only if you do not want to have mobile clients, you do not actually have to have android sdk or xcode to go on with your project
Quick start
cd /your/meteor/app
meteor npm install --save-dev @a4xrbj1/meteor-desktop
meteor add-platform ios # or android
npm run desktop -- init
meteor
# open new terminal
npm run desktopThe first time, you can also combine npm run desktop -- init and npm run desktop into npm run desktop -- --scaffold once meteor is running.
Usage --help
// Assuming you have a `desktop` script in npm scripts that equals to "@a4xrbj1/meteor-desktop"
Usage: npm run desktop -- [command] [options]
Commands:
init scaffolds .desktop dir in the meteor app
run [ddp_url] (default) builds and runs desktop app
build [ddp_url] builds your desktop app
build-installer [ddp_url] creates the installer
just-run alias for running `electron .` in `.meteor/desktop-build`
package [ddp_url] runs electron packager
init-tests-support prepares project for running functional tests of desktop app
Options:
-h, --help output usage information
-b, --build-meteor runs meteor to obtain the mobile build, kills it after
-t, --build-timeout <timeout_in_sec> timeout value when waiting for meteor to build, default 600sec
-p, --port <port> port on which meteor is running, when with -b this will be passed to meteor when obtaining the build
--production builds meteor app with the production switch, uglifies contents of .desktop, packs app to app.asar
-a, --android force adding android as a mobile platform instead of ios
-s, --scaffold will scaffold .desktop if not present
-i, --ignore-stderr [string] only with -b, strings that when found will not terminate meteor build
--meteor-settings <path> only with -b, adds --settings options to meteor
--prod-debug forces adding dev tools to a production build
--ia32 generate 32bit installer/package
--all-archs generate 32bit and 64bit installers
--win generate Windows installer
--linux generate Linux installer
--mac generate Mac installer
-d, --debug run electron with debug switch
-V, --version output the version number
[ddp_url] - pass a ddp url if you want to use a different one than the default
this will also work with -b--build-meteor
If you just want to build the desktop app, package it or build installer without running the
Meteor project separately you can just use -b and all will be done automatically - this is useful when
for example building on a CI etc.
--android
When there is no mobile platform in the project and -b is used, mobile platform is added
automatically and removed at the end of the build process. Normally an ios platform is added
but you can change this to android through this option.
Documentation
- Architecture
- Scaffolding your desktop app
- Writing modules
- Hot code push support
- Meteor.isDesktop
- Accessing local filesystem in Meteor
- Accessing .desktop/assets in Meteor
- Desktop and Module - communication between Meteor and Electron
- How to write plugins
- Squirrel autoupdate support
- Native modules support
- Testing desktop app and modules
- MD_LOG_LEVEL
- Packaging
- Building installer
- Contribution
- Built with meteor-desktop
- FAQ
- Changelog
Architecture
If you have ever been using any Cordova plugins before you will find this approach alike. In Cordova every plugin exposes its native code through a JS api available in some global namespace like cordova.plugins. The approach used here is similar.
In Electron app, there are two processes running along in your app. The so-called main
process and renderer process. Main process is just a JS code executed in node, and the
renderer is a Chromium process. In this integration your Meteor app is being run in the
renderer process and your desktop specific code runs in the main process. They are
communicating through IPC events. Basically, the desktop side publishes its API as an IPC event
listeners. In your Meteor code, calling it is as simple as Desktop.send('module', 'event');.
Code on the desktop side is preferred to be modular - that is just for simplifying testing and
encapsulating functionalities into independent modules. However, you do not have to follow this style, there is an import dir in which you can structure your code however you want. The basics of an Electron app are already in place (reffered as Skeleton App) and your code is loaded like a plugin to it.
Below is a high level architecture diagram of this integration.

How does this work with Meteor?
or how hacky is this?
The main goal was to provide a non-hacky integration without submitting desktop-specific pull
requests to Meteor.
The whole concept is based on taking the web.browser build, modifying it as little as possible,
and running it in Electron's renderer process.
In v5.0.0, the build pipeline works as follows:
- Manifest acquisition —
program.jsonis read directly from the on-diskweb.browserbuild output viafs.readFileSync(no HTTP download required). - ESM patching — JS responses from the Meteor dev server are patched for
import.meta,global, and classic-script constraints so they run correctly in Electron's renderer. - Hash coherence — a validation gate (A2.5) checks that bundle hashes are consistent before packaging, catching stale or mismatched builds early.
import.metapolyfill — injected into production bundles so Meteor 3.x ESM output runs in Electron without errors.Meteor.isDesktopinjection — theisDesktopInjectorbuild plugin injects theisDesktopflag into the web.browser bundle at build time.
How the Electron app is structured?
The produced Electron app consists barely of 4 files:
app.asar- bundledSkeleton Appandnode_modules(including all your dependencies fromsettings.jsonand modules)meteor.asar- yourMeteorapp bundled to an.asardesktop.asar- processed contents from.desktoppackage.json-Electronrequires apackage.jsonto be present
While developing, the app is not asared so you can take a closer look at the Skeleton that is
produced by this integration. You will find it in the .meteor/desktop-build directory.
Where is my app.on('ready')?
The app.on('ready') is handled for you by the Skeleton app, but that does not mean you can
not hook into it. Basically, code that is in the constructor of .desktop/desktop.js and
all constructors of your modules is executed while being inside ready. Remember that is always
a good practice not to do time consuming tasks inside the constructors but instead delay those tasks
by hooking to beforeDesktopJsLoad, desktopLoaded or afterInitialization on the eventsBus.
Scaffolding your desktop app
If you have not run the example from the Quick start paragraph, first you need to scaffold a
.desktop dir in which your Electron's main process code lives.
To do that run: (assuming npm install --save-dev meteor-desktop did add successfully a desktop
entry in the package.json scripts section)
npm run desktop -- initThis will generate an exemplary .desktop dir. Lets take a look what we can find there:
.desktop
├── assets # place all your assets here
├── import # all code you do not want to structure into modules
├── modules # your desktop modules (check modules section for explanation)
│ └── example # module example
│ ├── index.js # entrypoint of the example module
│ ├── example.test.js # functional test for the example module
│ └── module.json # module configuration
├── desktop.js # your Electron main process entry point - treated like a module
├── desktop.test.js # functional test for you desktop app
├── settings.json # your app settings
└── squirrelEvents.js # handling of squirrel.windows eventsTak a look into the files. Most of them have meaningful comments inside.
Some files are described more in detail below..
settings.json
This is the main configuration file for your desktop app. Below you can find brief descriptions of the fields.
field|description
-----|-----------
name|just a name for your project
version|version of the desktop app
projectName|this will be used as a name in the generated app's package.json
devTools|whether to install and open devTools, set automatically to false when building with --production
singleInstance|sets the single instance mode - more
rebuildNativeNodeModules|turn on or off recompiling native modules, more
webAppStartupTimeout|amount of time after which the downloaded version is considered faulty if Meteor app did not start - more
exposeLocalFilesystem|turns on or off local filesystem exposure over url alias, more
exposedModules|array of module names, exposes any renderer modules in Desktop.electron space, i.e. list webFrame here to acess it via Desktop.electron.webFrame in Meteor project code
showWindowOnStartupDidComplete|normally, main window appears after Chromes did-stop-loading event, set this to true if you want to depened on Meteor's startupDidComplete event
window|production options for the main window - see here
windowDev|development options for the main window, applied on top of production options
uglify|whether to process the production build with uglify
plugins|meteor-desktop plugins list
dependencies|npm dependencies of your desktop app, the same like in package.json, only explicit versions are supported - check here
linkPackages|array of packages names you want to link (runs npm link <packageName> for every package listed)
packageJsonFields|fields to add to the generated package.json in your desktop app
builderOptions|electron-builder options
builderCliOptions|specify additional electron-builder CLI options e.g for publishing artifacts
packagerOptions|electron-packager options
extract|array containing dependencies that should not be packed into asar (should not be needed as there is an automatic algorithm that will exclude all dependencies containing binary files)
Applying different window options for different OS
You can use _windows, _osx, _linux properties to set additional settings for different OS.
The default settings.json is already using that for setting a different window icon for OSX.
Supported dependency version types
Only explicit versions are supported to avoid potential problems with different versions being
installed. It is no different from Meteor because the same applies to adding desktop plugins.
You can however use a local path to a npm package - and that will not be forbidden. You need to keep track what has been distributed to your clients and what your current code is expecting when releasing a HCP update.
desktop.js
The desktop.js is the entrypoint of your desktop app. Let's take a look what references we
receive in the constructor.
/**
* @param {Object} log - Winston logger instance
* @param {Object} skeletonApp - reference to the skeleton app instance
* @param {Object} appSettings - settings.json contents
* @param {Object} eventsBus - event emitter for listening or emitting events
* shared across skeleton app and every module/plugin
* @param {Object} modules - references to all loaded modules
* @param {Object} Module - reference to the Module class
* @constructor
*/
constructor({ log, skeletonApp, appSettings, eventsBus, modules, Module })Some of the references are describe in detail below:
skeletonApp
This is a reference to the Skeleton App. Currently there are only two methods you can call.isProduction - whether this is a production buildremoveUncaughtExceptionListener - removes the default handler so you can put your own in place
eventsBus
This is just an EventEmitter that is an event bus meant to be used across all entities running
in the Electron's main process (.desktop). Currently there are several events emitted on the
bus by the Skeleton App that you may find useful:
event name|payload|description
----------|-------|------------
unhandledException| |emitted on any unhandled exceptions, by hooking to it you can run code before any other handler will be executedbeforePluginsLoad| |emitted before plugins are loaded
beforeModulesLoad| |emitted before internal modules and modules from .desktop are loaded
beforeDesktopJsLoad| |emitted before desktop.js is loaded
beforeLocalServerInit| |emitted before local http server starts
desktopLoaded|(desktop)|emitted after loading desktop.js, carries the reference to class instance exported from it
afterInitialization| |emitted after initialization of internal modules like HCP and local HTTP server
startupFailed| |emitted when the Skeleton App could not start you Meteor appbeforeLoadFinish| |emitted when the Meteor app finished loading, but just before the window is shownloadingFinished| |emitted when the Meteor app finished loading (also after HCP reload)windowSettings|(windowSettings)|emitted with the settings that will be passed to BrowserWindow constructor - if needed the object can be modified in the event handler to override window settings from settings.jsonwindowCreated|(window)|emitted when the BrowserWindow (Chrome window with Meteor app) is created, passes a reference to this window
newVersionReady|(version, desktopVersion)|emitted when a new Meteor bundle was downloaded and is ready to be appliedrevertVersionReady|(version)|emitted just before the Meteor app version will be reverted (due to faulty version fallback mechanism) be appliedbeforfeLoadUrl|(port, lastPort)|emitted before webContents.loadURL is invoked, in other words just before loading the Meteor app; port - the port on which the app is served, lastPort - the port on which the app was served previously (when HCP is applied)
beforeReload|(pendingVersion, containsDesktopUpdate)|emitted just before HCP reload
moduleLoadFailed|(dirName, error)|emitted if a module failed to load
childWindow|(openWindowHandlerResult, details)| emitted when child window is created. Gives possibility to reject or set up BrowserWindowConstructorOptions. For details see setWindowOpenHandler
Your can also emit events on this bus as well. A good practice is to namespace them using dots,
like for instance myModule.initalized.
modules
Object with references to other modules and plugins. Plugins can be found under their names i.e.,
modules['meteor-desktop-splash-screen].
Any module can be found under the name from module.json.
Internal modules such as autoupdate and localServer are also there. You can also get reference to the desktop.js from modules['desktop'] (note that the reference is also passed in
the desktopLoaded event).
Module
Class that provides a way of defining API reachable by Meteor app - more.
Writing modules
Module is just an encapsulated piece of code. Usually you would just provide certain type of
grouped functionality in it. You can treat it like a plugin to your desktop app.
One important rule is that you should not import files from the outside of your module directory
as this will cause you problems when writing tests.
You can always reach to other modules through modules and you can as well add a module with
some common code or utils.
Every module lives in its own directory and has to have a module.json file. Currently there are
only four fields there supported:
name- name of your module, will be used as a key inmodulesobjectdependencies- list of npm depsextract- list of files that should be excluded from packing into.asar(e.g. executables, files meant to be changed etc)settings- this object is passed assettingsfield in the object passed to module constructor
extract
A little bit more about this. Files should be listed in a form of relative path to the module
directory without any leading slashes, for example extract: [ "dir/something.exe" ] will be
matched to .desktop/modules/myModule/dir/something.exe.
To path to your extracted files is added to your module settings as extractedFilesPath
. So your module constructor can look like this:
import path from 'path';
export default class Desktop {
constructor({ log, skeletonApp, appSettings, eventsBus, modules, settings, Module }) {
this.pathToExe = path.join(settings.extractedFilesPath, 'dir/something.exe');
}
}WARNING: currently the path of the file is not reconstructed meaning extract: [ "dir1/something.exe", "dir2/something.exe' ] will try to put both something.exe files to the same dir and that may fail or produce inconsistent result. So the bare file names without the path must be unique.
Hot code push support
Applications produced by this integration are fully compatible with Meteor's hot code push
mechanism for web.browser content (packages, templates, styles). The faulty version recovery is also in place - more about it here. You can configure the timeout via
webAppStartupTimeout field in settings.json.
Versions are downloaded and served from userData directory.
There you can find autoupdate.json and versions dir. If you want to return to first
bundled version just delete them.
You can also analyze autoupdate.log if you are experiencing any issues.
Note:
.desktophot code push (desktopHCP) was removed in v6.0.0 because it is incompatible with rspack-based Meteor 3.x projects. Meteor's standard web.browser HCP continues to work. If you need desktopHCP, fork v5.1.7.
Meteor.isDesktop
In your Meteor app to run a part of the code only in the desktop context you can use Meteor.isDesktop. Use it the same way you would use Meteor.isClient or Meteor.isCordova.
Accessing local filesystem in Meteor
Local filesystem is exposed under and url alias (similarly to Meteor mobile integration).
This feature is disabled by default so you need to enable it first by setting
exposeLocalFilesystem in your settings.json to true. Files are exposed under
/local-filesystem/<absolute-path> url.
You can use some convenience methods:
Desktop.getFileUrl(absolutePath)- returns an url to a fileDesktop.fetchFile(absolutePath)- invokesfetchon a file's url and returns it'sPromise
Accessing .desktop/assets in Meteor
Assets are exposed over an url alias \___desktop\<asset-path>.
So to display an image named test.png from .desktop/assets you should use a
\___desktop\test.png url.
You can use some convenience methods:
Desktop.getAssetUrl(assetPath)- returns an asset's urlDesktop.fetchAsset(assetPath)- invokesfetchon an asset's url and returns it'sPromise
Desktop and Module - communication between Meteor and Electron
Module - desktop side
Use it to declare your API on the desktop side which you can later call from Meteor project.
this.module = new Module('myModuleName');Documentation of the Module API - basically, it reflects ipcMain.
The only two additions are the fetch and respond methods:
- fetch
(event, timeout = 2000, ...args)- like send but returns aPromisethat resolves to a response, timeouts after 2000ms by default - call
(module, ...args)-fetchbut without the need specify timeout - setDefaultFetchTimeout
(timeout)- set the default timeout forfetchwithin this module - respond
(event, fetchId, ...data)is a convenient method of sending response toDesktop.fetch. ThefetchIdis always the second argument received inon.
Here is an usage example.
Desktop - Meteor side
Documentation of the Desktop API - reflects partially ipcRenderer*.
* sendSync and sendToHost are not available
Use it to call and listen for events from the desktop side.
The only difference is that you always need to precede arguments with module name. There are two extra methods:
- fetch
(module, event, timeout = 2000, ...args)- like send but returns aPromisethat resolves to a response, timeouts after 2000ms by default - call
(module, event, ...args)-fetchbut without the need specify timeout - setDefaultFetchTimeout
(timeout)- set the default timeout forfetch - respond
(module, event, fetchId, ...data)is a convenient method of sending response toModule.fetch. ThefetchIdis always the second argument received inon. - sendGlobal - alias for
ipcRenderer.send- if you need to send an IPC that is not namespaced
Example of send and fetch usage - here.
How to write plugins
Plugin is basically a module exported to a npm package. module.json is not needed and not taken
into account because name and dependencies are already in package.json. Also you can not use
the extract functionality as that only works in modules. Plugin settings are set and taken
from the plugins section of settings.json. Here is an example of passing settings to splash
screen plugin.
While developing you will probably need to make use of linkPackages in settings.json, so that
your npm-packaged plugin would be linked instead of downloaded. However the advised approach is
to make the development test driven - meaning that you should make your tests the main way of
verifying whether the plugin does what it should.
meteorDependencies in package.json
One extra feature is that you can also depend on Meteor packages through meteorDependencies
field in package.json. Check out meteor-desktop-localstorage for example.
A good practice when your plugin contains a meteor plugin is to publish both at the same version.
You can then use @version in the meteorDependecies to indicate that the Meteor plugin's
version should be equal to npm package version.
If you made a plugin, please let us know so that it can be listed here.
List of known plugins:
meteor-desktop-system-notificationsmeteor-desktop-splashscreenmeteor-desktop-localstorage (deprecated, do not use from 1.0.0)
Native modules support
This integration fully supports rebuilding native modules (npm packages with native node modules)
against Electron's node version. The mechanism is enabled by default.
Testing desktop app and modules
For unit tests you should not have problems with using electron-mocha.
For functional testing, we recommend using Playwright or WebdriverIO.
There are two exemplary tests present in the default scaffold. Check them out as they have some
comments in them.
To run them you need to init functional test support by invoking:
npm run desktop -- init-tests-supportTwo tasks should be added to your scripts section: test-desktop and test-desktop-watch.
Feel free to run the tests with: npm run test-desktop.
Check the scaffold test files for usage examples.
MD_LOG_LEVEL
MD_LOG_LEVEL env var is used to set the logger verbosity. It is set to
ALL by default but you can change it to any of INFO, WARN, ERROR, DEBUG, VERBOSE, TRACE. You can also
select multiple levels joining them with a comma, for example: INFO,WARN.
Packaging
npm run desktop -- package <ddp-url>
This produces a package using electron-packager.
Package is produced and saved in .desktop-package directory. You can pass options via packagerOptions in
settings.json.
Building installer
npm run desktop -- build-installer <ddp-url>
This packages and builds installer using electron-builder.
Installer is produced and saved in .desktop-installer directory. You can pass options via
builderOptions in settings.json.
If you do not pass any target platforms via --win, --linux or --mac it will build for your
current platform. If at least one the platform is specified, the current platform will not be
added automatically. So if you want to build Windows and Mac at the same time, being on Mac,
you need to pass --win --mac, not only --win. To check what targets you can build on certain platform and what does it require
check Multi-Platform-Build
Please note that electron-builder does not use electron-packager to create a package. So the
options from packagerOptions are not taken into account.
Building for linux
Currently there are some defaults provided only for Windows and Mac. If you want to build for
Linux you need to add a linux section in your builderOptions and comply to these
requirements.
Building for Windows Store (AppX)
Change target: ["appx"] in win section of builderOptions. In case of problems please refer to
electron-builder documentation.
Developing meteor-desktop
Using devEnvSetup.js
To help you contribute, there is a development environment setup script. It also runs default tests.
This script assumes you have npm, git and meteor available from the command line. Note that by default, the script will install packages in the parent directory of where it is, so it is recommended to clone meteor-desktop inside an empty dir.
mkdir meteor-desktop-dev && cd meteor-desktop-dev
git clone https://github.com/a4xrbj1/meteor-desktop.git
cd meteor-desktop
npm install
node devEnvSetup.jsWithout using devEnvSetup.js
- Clone and install meteor-desktop as above
- From a clean Meteor project, install meteor-desktop from its local folder:
meteor npm i --save-dev /path/to/meteor-desktop(doesn't work with npm link, tbc) - In your Meteor app's
package.json:- Add a script
desktopwithMETEOR_PACKAGE_DIRS=/path/to/meteor-desktop/plugins node /path/to/meteor-desktop/lib/bin/cli.js - Add
METEOR_PACKAGE_DIRS=/path/to/meteor-desktop/pluginsto thestartscript as well.
- Add a script
The last step is so that the desktop HCP Meteor packages are also taken from your local meteor-desktop repo.
Make sure to run Meteor with meteor npm start rather than just meteor.
Finally, follow the above "Quick start" steps (except the npm install) from your Meteor app.
Built with meteor-desktop
Built an app using meteor-desktop? File an issue or PR to list it here.
FAQ
How to disable
zipbuilding when usingbuild-installeron OSX.
Add target: ["dmg"] to mac section of builderOptions.
Legacy / Deprecated Features
Squirrel autoupdate support (DEPRECATED)
Squirrel Window and OSX autoupdates are supported. So far the only tested server is
electron-release-server (Legacy/Unmaintained) and the
default url http://127.0.0.1/update/:platform/:version provided in settings.json assumes you
will be using it.
The :platform and :version tags are automatically replaced by correct values.
You can hook into Squirrel Windows events in squirrelEvents.js in .desktop.
More:
https://www.electronjs.org/docs/latest/api/auto-updater
https://github.com/ArekSredzki/electron-release-server
Deprecated Settings (settings.json)
These settings are deprecated and may be removed in future versions.
field|description
-----|-----------
squirrel.autoUpdateFeedUrl| DEPRECATED url passed to autoUpdater.setFeedUrl, more
squirrel.autoUpdateFeedHeaders| DEPRECATED http headers passed to autoUpdater.setFeedUrl
squirrel.autoUpdateCheckOnStart| DEPRECATED whether to check for updates on app start
Changelog
is here
