How to Setup Electron With ReScript

by Tom O'Connor

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.

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 = 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.

Let's Get To Work

Reach out and tell me about your website or product. I will be happy to listen.