-
-
Notifications
You must be signed in to change notification settings - Fork 76
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #169 from barischj/feature/electron
Add instructions for packaging with Electron
- Loading branch information
Showing
3 changed files
with
199 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
# Electron | ||
How to run and package Threepenny apps with [Electron](https://electron.atom.io) | ||
and | ||
[Electron Packager](https://github.com/electron-userland/electron-packager#electron-packager). | ||
|
||
For reference, a minimal working example is available | ||
[here](https://github.com/barischj/threepenny-gui-electron-example). | ||
|
||
## Justification | ||
Normally when running a Threepenny app we execute our Haskell, with `stack exec` | ||
or otherwise, which starts a local server and we open our browser on a certain | ||
port to view our app. However this has a few subtle drawbacks. | ||
|
||
One drawback is that most browsers are designed with remote servers in mind and | ||
have security features which don't make much sense for local server | ||
architectures like Threepenny's. Take file selection for example. When a user | ||
selects a file in a browser, the browser only exposes the file name and contents | ||
to the server, not allowing the server to receive the full file path. However | ||
for local server architectures there isn't much point in sending the entire file | ||
contents to the server since the server is on the same file system and could | ||
read the file directly, if only it had the full file path. Electron displays our | ||
app in a Chromium instance with many of these security features removed. | ||
|
||
Another drawback is that the user has to run the app from the command line. | ||
Using electron-packager we can package native apps for Linux, macOS and Windows. | ||
|
||
## Running with Electron | ||
To run a Threepenny app with Electron we need an Electron | ||
[main process](https://electron.atom.io/docs/tutorial/quick-start/#main-process). | ||
We provide this one: [electron.js](./electron/electron.js). It runs the | ||
following on startup: | ||
- selects a free port to run on | ||
- executes our Threepenny app binary, passing the port to run on as an argument | ||
- waits for Threepenny's server to start accepting connections | ||
- opens an Electron window which loads the URL of our Threepenny app | ||
|
||
To get started with the linked `electron.js` first add | ||
this [package.json]('./electron/package.json') to your project root directory. | ||
You'll need Node installed and `npm` on your PATH, confirm by running `which | ||
npm`. Now run `npm install` from the project root directory to install the | ||
necessary dependencies. | ||
|
||
The linked `electron.js` executes the Threepenny app binary, passing the port to | ||
run on as an argument. This of course means your Threepenny app needs to take | ||
the port as an argument. We suggest also setting stdout to be line buffered, at | ||
least while developing. Altogether it should look something like this: | ||
|
||
```Haskell | ||
module Main where | ||
|
||
import System.Environment (getArgs) | ||
import System.IO | ||
import YourApp (start) | ||
|
||
main :: IO () | ||
main = do | ||
hSetBuffering stdout LineBuffering | ||
[port, otherArg1, otherArg2] <- getArgs | ||
start (read port) | ||
``` | ||
|
||
Now copy the linked `electron.js` to your project root directory. You'll have to | ||
edit the defined constants: `relBin`, which is the relative path from | ||
`electron.js` to your Threepenny application binary; and `binArgs`, which | ||
contains any additional arguments to pass to the binary. If you're not sure | ||
about the relative path to your application binary, and you're using Stack, see | ||
the [next section](#explicit-binary-location). | ||
|
||
Now run your app with Electron: `./node_modules/.bin/electron electron.js` | ||
|
||
### Explicit binary location | ||
`relBin` is the relative path from `electron.js` to your Threepenny application | ||
binary. This might change depending on which tool or platform you are building | ||
with and thus can be a pain to set manually. If you are using Stack you can | ||
easily build your application binary to an explicit location, possibly a `build` | ||
directory: | ||
|
||
```stack install --local-bin-path build``` | ||
|
||
Now you can simply set `relBin` to `./build/your-app-exe`. | ||
|
||
## Packaging with electron-packager | ||
This section assumes the app is already setup to run with Electron based on | ||
the [above](#running-with-electron) instructions. | ||
|
||
First install electron-packager: `npm install electron-packager` | ||
|
||
Optionally edit the "name" field in `package.json` to set the name of the | ||
packaged app. Then to package the app for the current platform, simply: | ||
|
||
``` | ||
./node_modules/.bin/electron-packager . | ||
``` | ||
|
||
This is the most basic way to package the app, it will copy the current | ||
directory to the packaged app. However you'll likely want to avoid copying some | ||
source files, which can be achieved with the `--ignore` flag. You might end up | ||
using: | ||
|
||
``` | ||
./node_modules/.bin/electron-packager . --ignore=app --ignore=src | ||
``` | ||
|
||
If you are using Stack and building your application binary to an explicit | ||
location, as explained [above](#explicit-binary-location), then you might want | ||
to also ignore `.stack-work/`. An icon can be set by passing the icon path to | ||
`--icon`, note that the icon format | ||
[depends on the platform](https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#icon). | ||
For more options use the `--help` flag. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
const { app, BrowserWindow } = require('electron'); | ||
const freeport = require('freeport'); | ||
const spawn = require('child_process').spawn; | ||
const path = require('path'); | ||
const waitOn = require('wait-on'); | ||
|
||
// Time to wait for Threepenny server, milliseconds | ||
const timeout = 10000; | ||
// Relative path to the Threepenny binary. | ||
const relBin = './build/your-app-exe'; | ||
// Additional arguments to pass to the Threepenny binary. | ||
const binArgs = ['otherArg1', 'otherArg2']; | ||
|
||
// Assign a random port to run on. | ||
freeport((err, port) => { | ||
if (err) throw err; | ||
|
||
const url = `http://localhost:${port}`; | ||
let child = null; // Server process we spawn and kill | ||
|
||
// Keep a global reference of the window object, if we don't, the window will | ||
// be closed automatically when the JavaScript object is garbage collected. | ||
let win; | ||
|
||
function createWindow() { | ||
// Create the browser window. | ||
win = new BrowserWindow({ | ||
width: 800, | ||
height: 600, | ||
webPreferences: { nodeIntegration: false }, | ||
}); | ||
|
||
console.log(`Loading URL: ${url}`); | ||
win.loadURL(url); | ||
|
||
// Emitted when the window is closed. | ||
win.on('closed', () => { | ||
// Dereference the window object for garbage collection. | ||
win = null; | ||
}); | ||
} | ||
|
||
// Called when Electron has finished initialization and is ready to create | ||
// browser windows. Some APIs can only be used after this event occurs. We | ||
// start the child process and wait before loading the web page. | ||
app.on('ready', () => { | ||
child = spawn(path.join(__dirname, relBin), [port].concat(binArgs)); | ||
child.stdout.setEncoding('utf8'); | ||
child.stderr.setEncoding('utf8'); | ||
child.stdout.on('data', console.log); | ||
child.stderr.on('data', console.log); | ||
child.on('close', code => | ||
console.log(`Threepenny app exited with code ${code}`)); | ||
|
||
// Wait until the Threepenny server is ready for connections. | ||
waitOn({ resources: [url], timeout }, (err_) => { | ||
if (err_) throw err_; | ||
createWindow(); | ||
}); | ||
}); | ||
|
||
// Quit when all windows are closed, unless on macOS. On macOS it is common | ||
// for applications and their menu bar to stay active until the user quits | ||
// explicitly with Cmd + Q | ||
app.on('window-all-closed', () => { | ||
if (process.platform !== 'darwin') { | ||
app.quit(); | ||
} | ||
}); | ||
|
||
// Kill the child process when quitting Electron. | ||
app.on('will-quit', () => child.kill()); | ||
|
||
app.on('activate', () => { | ||
// On macOS it's common to re-create a window in the app when the dock icon | ||
// is clicked and there are no other windows open. | ||
if (win === null) { | ||
createWindow(); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"name": "YourApp", | ||
"main": "electron.js", | ||
"dependencies": { | ||
"electron": "^1.6.1", | ||
"freeport": "^1.0.5", | ||
"wait-on": "^2.0.2" | ||
} | ||
} |