Why is contextBridge not being imported in my Electron app?

Viewed 936

I'm writing an Electron app with React and Typescript, using Webpack, Babel and ESLint but I'm having trouble setting:

  mainWindow = new BrowserWindow({
    title: "Biomech",
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
      enableRemoteModule: false,
      sandbox: true,
      preload: path.join(__dirname, "./preload.js"),
      nativeWindowOpen: true,
    },
  });

which I want as a security measure.

The reason being that, if I set the webPreferences as specified above, I need to use contextBridge and a preload script that binds the functions that are using IPC to the window. And the problem is that contextBridge is not being properly imported in my preload.ts:

import { contextBridge, ipcRenderer } from "electron";
import { readFileSync } from 'fs';
import { History } from 'history';
import { LIST_DRIVE_FILES_CHANNEL, LOAD_PREV_TEST_RESULTS_CHANNEL, OAUTH2_ACCESS_TOKEN_REQUEST_CHANNEL } from "../src/constants/ipcChannels";
import { VISUALIZE_RESULTS_PATH } from "../src/constants/urls";


// Expose protected methods that allow the renderer process to use
// the ipcRenderer without exposing the entire object
contextBridge.exposeInMainWorld(
    "api", {
        exchangeCodeForAccessToken: (code: string) => {
            ipcRenderer.invoke(OAUTH2_ACCESS_TOKEN_REQUEST_CHANNEL, code);
        },
        listDriveFiles: (accessToken: string) => {
            ipcRenderer.invoke(LIST_DRIVE_FILES_CHANNEL, accessToken);
        },
        openDirectoryDialog: (history: History) => {
            ipcRenderer.invoke(LOAD_PREV_TEST_RESULTS_CHANNEL).then((dialog: any) => {
                if (dialog.canceled) {  // canceled with one l is correct
                    return;
                } else {
                    const selectedDirectory = dialog.filePaths[0];
                    console.log(readFileSync(selectedDirectory + "/README.md", 'utf-8'));
                    history.push(VISUALIZE_RESULTS_PATH);
                }
            })
        }
    }
);

The way I see it it's properly used like the examples I've seen here in SO, or in the Electron docs. But when I run my main process script: npm run dev:electron which is specified in my package.json:

{
  "name": "electron-react-ts-app",
  "version": "1.0.0",
  "description": "Electron + React + Typescript",
  "main": "./dist/main.js",
  "preload": "./dist/preload.js",
  "scripts": {
    "dev": "concurrently --success first \"npm run dev:electron\" \"npm run dev:react\" -k",
    "dev:electron": "NODE_ENV=development webpack --config webpack.electron.config.js --mode development && electron .",
    "dev:react": "NODE_ENV=development webpack serve --config webpack.react.config.js --mode development",
    "build:electron": "NODE_ENV=production webpack --config webpack.electron.config.js --mode production",
    "build:react": "NODE_ENV=production webpack --config webpack.react.config.js --mode production",
    "build": "npm run build:electron && npm run build:react",
    "pack": "electron-builder --dir",
    "dist": "electron-builder",
    "lint": "eslint .",
    "format": "prettier --write \"**/*.+(js|jsx|json|yml|yaml|css|md|vue)\""
  },
  "keywords": [],
  "license": "MIT",
  "build": {
    "files": [
      "dist/",
      "node_modules/",
      "package.json"
    ],
    "productName": "Example",
    "appId": "com.example.app",
    "directories": {
      "output": "dist"
    }
  },
  "devDependencies": {
    "@babel/preset-env": "^7.9.5",
    "@babel/preset-react": "^7.9.4",
    "@babel/preset-typescript": "^7.9.0",
    "@types/electron-devtools-installer": "^2.2.0",
    "@types/react-router-dom": "^5.1.7",
    "@types/regenerator-runtime": "^0.13.0",
    "dpdm": "^3.6.0",
    "electron": "^11.2.1",
    "electron-builder": "^22.7.0",
    "electron-devtools-installer": "^3.1.1",
    "eslint": "^7.18.0",
    "file-loader": "^6.2.0",
    "html-webpack-plugin": "^4.5.1",
    "husky": "^4.3.8",
    "lint-staged": "^10.5.3",
    "prettier": "^2.2.1",
    "react-router-dom": "^5.2.0",
    "webpack": "^5.11.1",
    "webpack-cli": "^4.3.1",
    "webpack-dev-server": "^3.11.1"
  },
  "dependencies": {
    "@babel/core": "^7.12.10",
    "@popperjs/core": "^2.6.0",
    "@types/node": "^14.14.22",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "axios": "^0.21.1",
    "babel-loader": "^8.2.2",
    "bootstrap": "^4.5.3",
    "chokidar": "^3.5.1",
    "core-js": "^3.8.3",
    "css-loader": "^5.0.1",
    "electron-fetch": "^1.7.3",
    "fsevents": "^2.3.1",
    "ini": "^2.0.0",
    "jquery": "^3.5.1",
    "react": "^17.0.1",
    "react-bootstrap": "^1.4.0",
    "react-dom": "^17.0.1",
    "react-google-login": "^5.2.2",
    "style-loader": "^2.0.0"
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run lint && npm run format"
    }
  },
  "lint-staged": {
    "*.+(js|jsx)": "eslint --fix",
    "*.+(json|css|md)": "prettier --write"
  }
}

I get the following error: Cannot read property 'exposeInMainWorld' of undefined

webpack 5.19.0 compiled successfully in 1793 ms
App threw an error during load
TypeError: Cannot read property 'exposeInMainWorld' of undefined
    at Object../electron/preload.ts (/Users/lucas_sg/Documents/ITBA/PF/pf-biomech/dist/preload.js:24:53)
    at __webpack_require__ (/Users/lucas_sg/Documents/ITBA/PF/pf-biomech/dist/preload.js:128:41)
    at /Users/lucas_sg/Documents/ITBA/PF/pf-biomech/dist/preload.js:252:11
    at Object.<anonymous> (/Users/lucas_sg/Documents/ITBA/PF/pf-biomech/dist/preload.js:254:12)
    at Module._compile (internal/modules/cjs/loader.js:1152:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1173:10)
    at Module.load (internal/modules/cjs/loader.js:992:32)
    at Module._load (internal/modules/cjs/loader.js:885:14)
    at Function.f._load (electron/js2c/asar_bundle.js:5:12738)
    at Module.require (internal/modules/cjs/loader.js:1032:19)

I've checked out this repo regarding Electron security and it doesn't look like he's doing things too different, at least at first glance, but I'm clearly messing something up.

Here's my webpack config (webpack.electron.config.js) in case it's useful:

const path = require("path");

module.exports = [
  {
    resolve: {
      extensions: [".tsx", ".ts", ".js"],
    },
    devtool: "source-map",
    entry: {
        main: {
            import: "./electron/main.ts",
            dependOn: "preload"
        },
        preload: "./electron/preload.ts"
    },
    output: {
      path: path.resolve(__dirname, "dist"),
      filename: "[name].js",
    },
    target: "electron-main",
    module: {
      rules: [
        {
          test: /\.(js|ts|tsx)$/,
          exclude: /node_modules/,
          use: {
            loader: "babel-loader",
          },
        },
        {
          test: /\.css$/,
          use: ["style-loader", "css-loader"],
        },
      ],
    },
    node: {
      __dirname: false,
    },
  },
];
1 Answers

I think the issue resides in you webpack.config file:

entry: {
    main: {
        import: "./electron/main.ts",
        dependOn: "preload"
    },
    preload: "./electron/preload.ts"
},

Might be that webpack tries to resolve all "preload.ts" dependency at compile time, plus tries to bundle it together with main.ts file. And in such case it will never get access to contextBridge. The preload.js file should only be run in a separate "context" - "Isolated World" as they refer in the docs.

What I would try is:

  1. remove the dependOn: and preload: lines from you webpack config.
  2. convert preload.ts file to JS (ie. to CommonJS format) - try doing that with TS CLI for now

Your preload.js file should look more or less like this:

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld(
  'electron',
  {
    doThing: () => ipcRenderer.send('do-a-thing')
  }
)
  1. copy preload.js to a __dirname (in my case it was /dist)
  2. run electron again

That was the only way I could access "contextBridge" object.

Related