Build a Meteor's desktop client with hot code push.
Build desktop apps with Meteor & Electron. Full integration with hot code push implementation.
This is a complete implementation of integration between
Meteorand
Electronaiming to achieve the same level of developer experience like
Meteorgives. 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.
1.4
*1 you can always build with
--server-onlyif 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
cd /your/meteor/app meteor npm install --save-dev meteor-desktop # you need to have any mobile platform added (ios/android) meteor --mobile-server=127.0.0.1:3000open new terminal
npm run desktop -- init npm run desktop
or in one command
npm run desktop -- --scaffold
--help
// Assumming you have a `desktop` script in npm scripts that equals to "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 different one than used in meteor's --mobile-server 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
Meteorproject separately you can just use
-band 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
-bis used, mobile platform is added automatically and removed at the end of the build process. Normally an
iosplatform is added but you can change this to
androidthrough this option.
Desktop
and Module
- communication between Meteor and Electron
If you have ever been using any
Cordovaplugins before you will find this approach alike. In
Cordovaevery plugin exposes its native code through a JS api available in some global namespace like
cordova.plugins. The approach used here is similar.
In
Electronapp, there are two processes running along in your app. The so-called
main processand
renderer process. Main process is just a JS code executed in
node, and the renderer is a
Chromiumprocess. In this integration your
Meteorapp is being run in the
rendererprocess and your desktop specific code runs in the
mainprocess. They are communicating through IPC events. Basically, the desktop side publishes its API as an IPC event listeners. In your
Meteorcode, 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
importdir in which you can structure your code however you want. The basics of an
Electronapp 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.
or how hacky is this?
The main goal was to provide a non hacky integration without actually submitting any desktop oriented pull request to
Meteor. The whole concept is based on taking the
web.cordovabuild, modifying it as little as possible and running it in the
Electron'srenderer process. The current
cordovaintegration architecture is more or less conceptually replicated.
Currently the only modification that the mobile build is subjected to is injecting the
Meteor.isDesktopvariable.
To obtain the mobile build, this integration takes the build from either
.meteor/local/cordova-build(version
< 1.3.4.1) or from
.meteor/local/build/programs/web.cordova. Because
index.htmlis not present in the
web.cordovadirectory and
program.jsonlacks
versionfield, they are just downloaded from the running project.
Electronapp is structured?
The produced
Electronapp consists barely of 4 files:
app.asar- bundled
Skeleton Appand
node_modules(including all your dependencies from
settings.jsonand modules)
meteor.asar- your
Meteorapp bundled to an
.asar
desktop.asar- processed contents from
.desktop
package.json-
Electronrequires a
package.jsonto be present
While developing, the
appis not asared so you can take a closer look at the
Skeletonthat is produced by this integration. You will find it in the
.meteor/desktop-builddirectory.
app.on('ready')?
The
app.on('ready')is handled for you by the
Skeletonapp, but that does not mean you can not hook into it. Basically, code that is in the constructor of
.desktop/desktop.jsand 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,
desktopLoadedor
afterInitializationon the
eventsBus.
If you have not run the example from the Quick start paragraph, first you need to scaffold a
.desktopdir in which your
Electron'smain process code lives. To do that run: (assuming
npm install --save-dev meteor-desktopdid add successfully a
desktopentry in the
package.json scriptssection)
bash npm run desktop -- init
This will generate an exemplary
.desktopdir. 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 events
Tak a look into the files. Most of them have meaningful comments inside.
Some files are described more in detail below..
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
namein the generated app's package.json
devTools|whether to install and open
devTools, set automatically to false when building with
--production
devtron|check whether to install
devtron, set automatically to false when building with
--production, more
singleInstance|sets the single instance mode - more
desktopHCP|whether to use
.desktophot code push module - more
desktopHCPIgnoreCompatibilityVersion|ignore the
.desktopcompatibility version and install new versions even if they can be incompatible
desktopHCPCompatibilityVersion|allows to override
.desktopcompatibility version
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
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.electronspace, i.e. list
webFramehere to acess it via
Desktop.electron.webFramein Meteor project code
showWindowOnStartupDidComplete|normally, main window appears after Chromes
did-stop-loadingevent, set this to
trueif you want to depened on Meteor's
startupDidCompleteevent
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 linkfor every package listed)
packageJsonFields|fields to add to the generated
package.jsonin your desktop app
builderOptions|
electron-builderoptions
builderCliOptions|specify additional electron-builder CLI options e.g for publishing artifacts
packagerOptions|
electron-packageroptions
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)
You can use
_windows,
_osx,
_linuxproperties to set additional settings for different OS. The default
settings.jsonis already using that for setting a different window icon for OSX.
Only explicit versions are supported to avoid potential problems with different versions being installed. It is no different from
Meteorbecause the same applies to adding
Cordovaplugins.
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.
The
desktop.jsis the entrypoint of your desktop app. Let's take a look what references we receive in the constructor.
javascript /** * @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 build
removeUncaughtExceptionListener- removes the default handler so you can put your own in place
eventsBus
This is just an
EventEmitterthat is an event bus meant to be used across all entities running in the
Electron'smain process (
.desktop). Currently there are several events emitted on the bus by the
Skeleton Appthat 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 executed
beforePluginsLoad| |emitted before plugins are loaded
beforeModulesLoad| |emitted before internal modules and modules from
.desktopare loaded
beforeDesktopJsLoad| |emitted before
desktop.jsis 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 Appcould not start you
Meteorapp
beforeLoadFinish| |emitted when the
Meteorapp finished loading, but just before the window is shown
loadingFinished| |emitted when the
Meteorapp finished loading (also after HCP reload)
windowSettings|
(windowSettings)|emitted with the settings that will be passed to
BrowserWindowconstructor - if needed the object can be modified in the event handler to override window settings from
settings.json
windowCreated|
(window)|emitted when the
BrowserWindow(
Chromewindow with
Meteorapp) is created, passes a reference to this window
newVersionReady|
(version, desktopVersion)|emitted when a new
Meteorbundle was downloaded and is ready to be applied
revertVersionReady|
(version)|emitted just before the
Meteorapp version will be reverted (due to faulty version fallback mechanism) be applied
beforfeLoadUrl|
(port, lastPort)|emitted before
webContents.loadURLis 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
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].
module.json. Internal modules such as
autoupdateand
localServerare also there. You can also get reference to the
desktop.jsfrom
modules['desktop'](note that the reference is also passed in the
desktopLoadedevent).
Module
Class that provides a way of defining API reachable by
Meteorapp - more.
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
modulesand 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.jsonfile. Currently there are only four fields there supported: -
name- name of your module, will be used as a key in
modulesobject -
dependencies- list of npm deps -
extract- 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 as
settingsfield 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
settingsas
extractedFilesPath. So your module constructor can look like this:
javascript 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.exefiles to the same dir and that may fail or produce inconsistent result. So the bare file names without the path must be unique.
Applications produced by this integration are fully compatible with
Meteor's hot code push mechanism.
webAppStartupTimeoutfield in
settings.json.
Versions are downloaded and served from
userDatadirectory. There you can find
autoupdate.jsonand
versionsdir. If you want to return to first bundled version just delete them.
You can also analyze
autoupdate.logif you are experiencing any issues.
Meteor.isDesktop
In your
Meteorapp 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.isClientor
Meteor.isCordova.
Local filesystem is exposed under and url alias (similarly to Cordova integration). This feature is disabled by default so you need to enable it first by setting
exposeLocalFilesystemin your
settings.jsonto
true. Files are exposed under
/local-filesystem/url.
You can use some convenience methods: -
Desktop.getFileUrl(absolutePath)- returns an url to a file -
Desktop.fetchFile(absolutePath)- invokes
fetchon a file's url and returns it's
Promise
.desktop/assetsin Meteor
Assets are exposed over an url alias
\___desktop\. So to display an image named
test.pngfrom
.desktop/assetsyou should use a
\___desktop\test.pngurl.
You can use some convenience methods: -
Desktop.getAssetUrl(assetPath)- returns an asset's url -
Desktop.fetchAsset(assetPath)- invokes
fetchon an asset's url and returns it's
Promise
Desktopand
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.
javascript this.module = new Module('myModuleName');Documentation of the Module API - basically, it reflects
ipcMain.
The only two additions are the
fetchand
respondmethods: - fetch
(event, timeout = 2000, ...args)- like send but returns a
Promisethat resolves to a response, timeouts after 2000ms by default - call
(module, ...args)-
fetchbut without the need specify timeout - setDefaultFetchTimeout
(timeout)- set the default timeout for
fetchwithin this module - respond
(event, fetchId, ...data)is a convenient method of sending response to
Desktop.fetch. The
fetchIdis always the second argument received in
on.
Desktop- Meteor side
Documentation of the Desktop API - reflects partially
ipcRenderer*.
*
sendSyncand
sendToHostare 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 a
Promisethat 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 for
fetch- respond
(module, event, fetchId, ...data)is a convenient method of sending response to
Module.fetch. The
fetchIdis always the second argument received in
on.
ipcRenderer.send- if you need to send an IPC that is not namespaced
Example of
sendand
fetchusage - here.
.desktophot code push
experimental!
There is an experimental support for hot code push of the
.desktopdirectory.
Meteor's builtin one. It also produces a
versionand
compatibilityVersionto detect whether the update can be made.
Meteorwhenever you change any of your
Cordovadependencies (add/remove/change version) you will make an incompatible change meaning that a new version will not be hot code pushed.
.desktopare.
The
compatibilityVersionis calculated from combined list of: - dependencies from
settings.json- plugins from
settings.json- dependencies from all modules in
.desktop/modules- major version of
meteor-desktop(X.Y.Z - only X is taken) - major version from
settings.json(X.Y.Z - only X is taken).
Be aware that when it comes to linked packages (via
linkPackagesin
settings.json) the explicitly declared version (the one in
settings.jsonor modules) is taken into account, not the actual one from package's package.json. The same applies to packages added from local paths.
desktopHCPthat will not work.
Two Meteor plugins are added to your project - bundler and watcher. Bundler prepares the
desktop.asarwhich is then added to you project as an asset.
desktop.asarfile will also be distributed to your mobile clients and cause unnecessary updates in case you only made changes in
.desktop
desktop.asar(via
extractsettings in a desktop module) are not updated, nor checked for changes!
.desktopwhich prevented startup, watcher might not work correctly and further changes in
.desktopwill not trigger rebuilds, in that case you need to make any change in
versionfield in the
desktop.versionto trigger rebuild (this file is in the root of your project) - this can be any change like just adding random char to the hash
meteorcommand unless you run it with
--production- that is because development build has
devtronadded and therefore the
compatibilityVersionis different
Plugin is basically a module exported to a npm package.
module.jsonis not needed and not taken into account because
nameand
dependenciesare already in
package.json. Also you can not use the
extractfunctionality as that only works in modules. Plugin
settingsare set and taken from the
pluginssection of
settings.json. Here is an example of passing settings to splash screen plugin.
While developing you will probably need to make use of
linkPackagesin
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.
meteorDependenciesin
package.json
One extra feature is that you can also depend on Meteor packages through
meteorDependenciesfield in
package.json. Check out
meteor-desktop-localstoragefor example.
@versionin the
meteorDependeciesto 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.
meteor-desktop-system-notifications
meteor-desktop-splashscreen
meteor-desktop-localstorage(deprecated, do not use from
1.0.0)
Squirrel Window and OSX autoupdates are supported. So far the only tested server is
electron-release-serverand the default url
http://127.0.0.1/update/:platform/:versionprovided in
settings.jsonassumes you will be using it.
:platformand
:versiontags are automatically replaced by correct values.
squirrelEvents.jsin
.desktop.
More:
https://github.com/electron/electron/blob/master/docs/api/auto-updater.md
https://github.com/ArekSredzki/electron-release-server
This integration fully supports rebuilding native modules (npm packages with native node modules) against
Electron's
nodeversion. The mechanism is enabled by default.
Devtronis installed and activated by default. It is automatically removed when building with
--production. As the communication between your Meteor app and the desktop side goes through IPC, this tool can be very handy because it can sniff on IPC messages.
For unit tests you should not have problems with using electron-mocha.
For functional testing Spectron should be used.
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
scriptssection:
test-desktopand
test-desktop-watch. Feel free to run the tests with:
npm run test-desktop.
For testing modules there is a test suite available. It is used extensively in the plugins (splash screen & localstorage) tests so you can check there for more examples.
MD_LOG_LEVEL
MD_LOG_LEVELenv var is used to set the logger verbosity. It is set to
ALLby 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.
npm run desktop -- package
electron-packager.
.desktop-packagedirectory. You can pass options via
packagerOptionsin
settings.json.
npm run desktop -- build-installer
This packages and builds installer using
electron-builder.
.desktop-installerdirectory. You can pass options via
builderOptionsin
settings.json.
--win,
--linuxor
--macit 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-builderdoes not use
electron-packagerto create a package. So the options from
packagerOptionsare not taken into account.
Currently there are some defaults provided only for
Windowsand
Mac. If you want to build for
Linuxyou need to add a
linuxsection in your
builderOptionsand comply to these requirements.
Change
target: ["appx"]in
winsection of
builderOptions. In case of problems please refer to electron-builder documentation.
This project recently hit
1.0.0however you should still expect many breaking changes in the upcoming versions. Any feedback/feature requests/PR is highly welcomed and highly anticipated.
If you want to check what is planned and what I am working on, first you can check accepted issues on github here. You can see the backlog and roadmap in form of epics on Taiga here. The project is public so you can also comment and vote there.
PRs are always welcome and encouraged. If you need help at any stage of preparing a PR, just file an issue. It is also good, to file a feature request issue before you start working to discuss the need and implementation approach.
If you want, you can always contribute by donating:
To help you contribute, there is a development environment setup script. If you have this repo cloned and already did a
npm install, you can just run it with
node devEnvSetup.js. However if you did not yet clone this repo just do:
mkdir tmp cd tmp wget https://raw.githubusercontent.com/wojtkowiak/meteor-desktop/master/devEnvSetup.js npm install cross-spawn shelljs npm node devEnvSetup.jsThis script assumes you have
npm,
gitand
meteoravailable from the command line.
Currently this package does not work when linked with
npm link. To set up your dev environment it is best to create a clean
Meteorproject, add
meteor-desktopto dependencies with a relative path to the place where you have cloned this repo and in scripts add
desktopwith
node ./path/to/meteor-desktop/dist/bin/cli.js.
Meteorproject with
METEOR_PACKAGE_DIRSset to
/absolute/path/to/meteor-desktop/pluginsso that they will be taken from the cloned repo.
meteor-desktop
Built an app using meteor-desktop? File an issue or PR to list it here.
How to disable
zipbuilding when usingbuild-installeron OSX.
Add
target: ["dmg"]to
macsection of
builderOptions.
is here