I was interested in using ReScript with Electron, but the only setup instructions I came across included settings like:
- nodeIntegration: true
- contextIsolation: false
- and lack of a Content-Security-Policy meta tag in the head of the HTML doc
- By using the aforementioned settings, Chrome gains access to Electron’s Node environment, and the frontend shares the same global space as the renderer.
Such configuration exposes our application to potential security vulnerabilities.
If our application doesn’t make any third-party remote calls or receive user input, this may not be an issue. But if we plan to share our starter app setup instructions with the community, we should follow the best practices recommended on the official Electron website Security, Native Capabilities, and Your Responsibility.
What follows are some instructions that allow us to enjoy the benefits of ReScript in our Electron applications while keeping our environment secure.
#1: Initial Setup
Initialize a package.json:
$ yarn init
Add all the NPM packages:
$ yarn add electron rescript -D && yarn add react react-dom @rescript/react
Add the following NPM commands to package.json:
"scripts": {
"electron": "electron .",
"rescript": "rescript build -w -with-deps"
}
#2: Adding index.html
On the frontend, Electron is just Chrome, so we will need an entry index.html to mount our app.
Create an index.html file in the root of your project:
$ touch index.html
And add the following code to it:
<!DOCTYPE html>
<html>
<head>
<title>Electron Rescript React Starter App</title>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="script-src 'sha256-3L+AVUCIZkUxl7KLiKIS32E3Djr8nFzTNRT6rus9ReI=';"
/>
<meta
http-equiv="X-Content-Security-Policy"
content="script-src 'sha256-3L+AVUCIZkUxl7KLiKIS32E3Djr8nFzTNRT6rus9ReI=';"
/>
</head>
<body>
<div id="root"></div>
<script>
window.api.loadApp()
</script>
</body>
</html>
The critical aspect of the above file is the Content-Security-Policy meta tag, which features a SHA256 hash that limits execution to window.api.loadApp().
Later in these instructions, we will include this function in the preload.js file of Electron to prevent exposing the backend NodeJs process to the frontend of our app.
#3: Adding Index.js
We need to add an index.js file in the root of our directory to bootstrap electron from:
$ touch index.js
And add the following code to it:
const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow
const path = require('path')
function createWindow() {
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false,
preload: path.join(__dirname, 'preload.js'),
},
})
mainWindow.loadFile('index.html')
}
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
app.quit()
})
#4: Adding Preload.js
Exposing require() in the frontend of our app poses a significant security risk.
The recommended approach to load an app in Electron is to expose a whitelisted wrapper around require() that can be safely used in the frontend Chrome instance.
If you look back to our index.html we are making the window.api.loadApp() call.
We will expose this call through Electron’s preload.js to then require and mount the rest of our app.
Create the preload.js file:
$ touch preload.js
And add the following code:
const electron = require('electron')
const contextBridge = electron.contextBridge
contextBridge.exposeInMainWorld(
'api', {
loadApp(){
require('./lib/js/src/index.js');
}
}
)
#5: Add and Configure ReScript
Now add a bsconfig.json file — this is where you can configure the different options for the ReScript complier:
$ touch bsconfig.json
And add the following object to it:
{
"name": "rescript-electron-setup",
"sources": [
{
"dir": "./src",
"subdirs": true
}
],
"package-specs": [
{
"module": "commonjs",
"in-source": false
}
],
"suffix": ".js",
"bs-dependencies": ["@rescript/react"],
"reason": { "react-jsx": 3 }
}
#6: Add the Index.res File and Mount React
Make a src folder and add an Index.res file; this will function as our main entry point to our ReScript App.
$ mkdir src && cd src && touch Index.res
Add the following code to it:
module App = {
@react.component
let make = () => {
<div> {React.string("ReScript Electron App Starter")} </div>
}
}
switch ReactDOM.querySelector("#root") {
| Some(root) => ReactDOM.render(<div> <App /> </div>, root)
| None => ()
}
#7: Starting the Dev Environment
We need to run 2 instances in our CLI. In one tab or window we can start the ReScript build process:
$ yarn run rescript
And in another we can start Electron:
$ yarn run electron
If everything has gone to plan then you should have a properly functioning ReScript app running in Electron.
Checkout this Electron ReScript React Starter App repo for reference and a working starter app.