Utilizing puppeteer-core with electron: A guide

I found this snippet on a different Stack Overflow thread:

import electron from "electron";
import puppeteer from "puppeteer-core";

const delay = (ms: number) =>
  new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, ms);
  });

(async () => {
  try {
    const app = await puppeteer.launch({
      executablePath: electron,
      args: ["."],
      headless: false,
    });
    const pages = await app.pages();
    const [page] = pages;

    await page.setViewport({ width: 1200, height: 700 });
    await delay(5000);
    const image = await page.screenshot();
    console.log(image);
    await page.close();
    await delay(2000);
    await app.close();
  } catch (error) {
    console.error(error);
  }
})();

The Typescript compiler is throwing an error for the executablePath property in the launch method's options object because it should be of type string rather than Electron. How can we correctly pass the chromium executable path to puppeteer?

Answer №1

Utilizing the electron executable with Puppeteer directly requires some workarounds and flag adjustments due to their differences in API. Electron lacks certain key functionalities found in the chrome.* API, essential for proper operation of Chromium browser. Many flags do not yet have suitable replacements, such as the headless flag.

Outlined below are two methods to achieve this. However, it is crucial to adhere to the following points:

  • Ensure that Puppeteer is connected before launching the application.
  • Verify you have the correct version of Puppeteer or Puppeteer-core matching the Chrome version running within Electron!

Option 1: Leveraging puppeteer-in-electron

Among various alternatives, a recent addition includes the puppeteer-in-electron package, enabling the running of Puppeteer inside an Electron app utilizing Electron itself.

To begin, install the necessary dependencies:

npm install puppeteer-in-electron puppeteer-core electron

Subsequently, execute the code snippet provided below.

import {BrowserWindow, app} from "electron";
import pie from "puppeteer-in-electron";
import puppeteer from "puppeteer-core";

const main = async () => {
  const browser = await pie.connect(app, puppeteer);

  const window = new BrowserWindow();
  const url = "https://example.com/";
  await window.loadURL(url);

  const page = await pie.getPage(browser, window);
  console.log(page.url());
  window.destroy();
};

main();

Option 2: Obtain the debugging port and establish connection

Alternatively, one can acquire the remote-debugging-port of the Electron app and connect to it. This approach was shared by trusktr on the Electron forum.

import {app, BrowserWindow, ...} from "electron"
import fetch from 'node-fetch'

import * as puppeteer from 'puppeteer'

app.commandLine.appendSwitch('remote-debugging-port', '8315')

async function test() {
    const response = await fetch(`http://localhost:8315/json/versions/list?t=${Math.random()}`)
    const debugEndpoints = await response.json()

    let webSocketDebuggerUrl = debugEndpoints['webSocketDebuggerUrl ']

    const browser = await puppeteer.connect({
        browserWSEndpoint: webSocketDebuggerUrl
    })

    // utilize Puppeteer APIs now!
}

// Establish your window, etc., then proceed with:

  // after window load, connect Puppeteer:
  mainWindow.webContents.on("did-finish-load", () => { 
    test()
  })

Both solutions above rely on the usage of webSocketDebuggerUrl to address the issue.

Additional Information

Since many individuals use Electron for bundling applications, it's worth mentioning:

If building puppeteer-core and puppeteer-in-electron, ensure the utilization of hazardous and electron-builder to facilitate functioning of get-port-cli.

Place hazardous at the beginning of main.js:

// main.js
require ('hazardous');

Unpack the get-port-cli script and specify in package.json:

"build": {
  "asarUnpack": "node_modules/get-port-cli"
}

Upon completion of the build process:

Answer №2

The solution provided did not work for me when using Electron 11 and Puppeteer-core 8. However, starting Puppeteer in the main process instead of the renderer process turned out to be effective. By utilizing ipcMain and ipcRenderer to communicate with each other, I was able to achieve the desired outcome. See the modified code below:

Main.ts (main process code)

import { app, BrowserWindow, ipcMain } from 'electron';
import puppeteer from 'puppeteer-core';
async function newGrabBrowser({ url }) {
  const browser = await puppeteer.launch({
    headless: false,
    executablePath:
      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
  });
  const page = await browser.newPage();
  page.goto(url);
}
ipcMain.on('grab', (event, props) => {
  newGrabBrowser(JSON.parse(props));
});

Home.ts (renderer process code)

const { ipcRenderer } = require('electron');
ipcRenderer.send('grab',JSON.stringify({url: 'https://www.google.com'}));

Answer №3

In addition to the previous method, there is another alternative that is compatible with electron versions 5.x.y and above (currently supported up to version 7.x.y, but not tested on beta version 8.x.y yet):

// const assert = require("assert");
const electron = require("electron");
const kill = require("tree-kill");
const puppeteer = require("puppeteer-core");
const { spawn } = require("child_process");

let pid;

const launchElectron = async () => {
  const port = 9200; // Debugging port
  const startTime = Date.now();
  const timeout = 20000; // Timeout in milliseconds
  let app;

  // Start Electron with custom debugging port
  pid = spawn(electron, [".", `--remote-debugging-port=${port}`], {
    shell: true
  }).pid;

  // Wait for Puppeteer to connect
  while (!app) {
    try {
      app = await puppeteer.connect({
        browserURL: `http://localhost:${port}`,
        defaultViewport: { width: 1000, height: 600 } // Optional I believe
      });
    } catch (error) {
      if (Date.now() > startTime + timeout) {
        throw error;
      }
    }
  }

  // Perform some actions, for example:
  // const [page] = await app.pages();
  // await page.waitForSelector("#someid")// 
  // const text = await page.$eval("#someid", element => element.innerText);
  // assert(text === "Your desired text");
  // await page.close();
};

launchElectron()
  .then(() => {
    // Carry out further actions
  })
  .catch(error => {
    // Handle errors
    kill(pid, () => {
      process.exit(1);
    });
  });

Obtaining the pid and utilizing kill is an optional step. It may not be necessary when running the script on a CI platform, but in a local environment you might need to manually close the electron application after each unsuccessful attempt.

For more details, please refer to this sample repository.

Answer №4

As stated in the documentation puppeteer-in-electron, everything should work properly. However, it is important to note that when working with ipcMain, you need to initialize the app before calling the main function.

main.js

// main.js
const {BrowserWindow, app} = require("electron");
const pie = require("puppeteer-in-electron")
const puppeteer = require("puppeteer-core");

app.whenReady().then(() => {
    ipcMain.handle('get-url', getUrl)
    createWindow()

    ///
})

async function initialize(){
    await pie.initialize(app);
}
initialize();

async function getUrl(){
  const browser = await pie.connect(app, puppeteer);
 
  const window = new BrowserWindow();
  const url = "https://example.com/";
  await window.loadURL(url);
 
  const page = await pie.getPage(browser, window);
  console.log(page.url());
  window.destroy();
  return page.url();
};

preload.js

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

contextBridge.exposeInMainWorld('electronAPI', {
    getUrl: () => ipcRenderer.invoke('get-url')
})

renderer (vue3)

<script setup>
import { ref } from 'vue';

const res = ref();

async function get(){
    const result = await window.electronAPI.getUrl()
    res.value = result
    console.log(result);
}
</script>

my packages version

  • "puppeteer-core": "^19.7.1"
  • "puppeteer-in-electron": "^3.0.5"
  • "electron": "^23.1.0"

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Searching dynamically using class names with JQuery

I am seeking to create a dynamic search input based on the class names within the span tags. However, I am struggling with displaying the class name that I have identified. My goal is to show the class names that match the entered value in the input on the ...

Angular JS effectively prevents redundant data from being displayed to users when scrolling infinitely while also efficiently removing DOM elements for previous data

I'm currently working on implementing AngularJS Infinite Scroll without using jQuery. My goal is to display the first 3 data items when the page loads and then load the next 3 data items from a JSON array object as the user scrolls. The issue I am fac ...

Using Backbone.sync to customize dataType

I am working on an application that utilizes a .CSV file as the primary data source for a Backbone Model. I am interested in finding the most effective approach to changing the sync method so that it uses dataType "text" instead of "json". Any insights on ...

The functionality of nested routing is not operating properly in react-router

I'm currently struggling to get my CollectionPage to render and match the URL correctly within my nested Route in React. Unfortunately, it doesn't seem to be working as expected! Here's a piece of code from my shop.component file that is be ...

Receiving an error in TypeScript stating that the property or data does not exist on the type for Vue props

I've recently integrated TypeScript into my Vue project, and now I'm encountering an error every time I try to access a value in props or data: 37:46 Property 'activity' does not exist on type '{ props: { activity: { type: ObjectC ...

Utilize React-router to display child components on the webpage

Recently, I've encountered some challenges with this code. I have a component that takes in a child as a prop, which is meant to serve as the foundation for all the pages I am hosting. Base.jsx : import React from 'react'; import PropTypes ...

Forwarding parameter data type

I am facing an issue with 2 navigation points leading to the same screen 1. this.router.navigate([this.config.AppTree.App.Module.Details.Path], { state: { data: { id: this.TableId } } }); this.router.navigate([this.config.AppTree.App.Module.Details.Pa ...

What is the method to retrieve the base host in AngularJS?

I need assistance with the following URL: https://192.168.0.10/users/#!/user-profile/20 When I use $location.host, it returns 192.168.0.10 However, I only want to extract https://192.168.0.10 What is the best way to achieve this? ...

The addition operation in JavaScript seems to be malfunctioning as it is not adding up my values

After encountering an issue with calculating the number of payments, I discovered that the addition operator was not functioning as expected. Instead of summing up values, it was treating them as strings. For instance, when trying to add 3 and 5, the out ...

Tips on aligning a div to the right of a headline

My goal is to have an orange circle image placed to the right of my headline <h2>. However, I am facing an issue where the circle jumps above the headline when I shrink the screen. How can I ensure that it stays on the right side no matter the screen ...

GetServerSideProps function yielding varied prop values

I'm currently exploring NextJS and delving into SSR. I've been struggling to grasp the functionality of getServerSideProps(). It seems that it should replace useState in order to be rendered on the backend, but I'm receiving different props ...

What methods can be used to get npx to execute a JavaScript cli script on a Windows operating system

The Issue - While my npx scaffolding script runs smoothly on Linux, it encounters difficulties on Windows. I've noticed that many packages run flawlessly on Windows, but the difference in their operations remains elusive to me. Even after consulting A ...

Converting JSONSchema into TypeScript: Creating a structure with key-value pairs of strings

I am working with json-schema-to-typescript and looking to define an array of strings. Currently, my code looks like this: "type": "object", "description": "...", "additionalProperties": true, "items": { "type": "string" ...

Select the first item that is visible and chosen

Currently, I am working with a select list: <option ng-repeat="hour in Hours" value="{{hour.Value}}" ng-show="filterEvent($index)" ng-selected="hour.Value == EventDate || $first"> {{hour.Text}} </opti ...

Integrate NodeJs into Ionic framework for enhanced functionality and dynamic

I created a hybrid application using Ionic framework. Currently, I have been using PHP and MySQL as the backend technology. However, after doing some research, I realized that Node.js with Express.js and MongoDB are considered more modern options. Most ...

Determine if every object in the array contains a specific key value pair

I am dealing with the following JSON data: { records:{ data:{ current:{ records:{ '2years':[{a:1, b:2, flag:true}, {a:2, b:4}, ...

Can an AJAX upload progress bar be implemented in Internet Explorer without using Flash?

I've been searching for solutions to upload files without using flash, but all of them either require flash or lack a progress bar on IE (7-8). I couldn't find any mention of an "progress" event in the MSDN documentation for XMLHTTPRequest. Is i ...

What is the best way to add a constant value to all objects within an array without having to iterate through each one

Is there a more concise way to add a fixed value to each object in an array without using a loop in JavaScript? Programming Language used: JavaScript Example Array: "cars": [ { "name":"Ford", "models":"Fiesta" }, { "name":"BMW", "models":"X1" }, ...

Providing structured Express app to deliver HTML and JavaScript content

Currently, I am working with Express and facing a seemingly simple challenge. Here is the structure of my directories: |-config |---config.js |---routes.js |-server.js |-scripts |---controllers |------controllers.js |---directive ...

Adding information to a single class that shares a common name

Currently, I have successfully implemented a row cloning scenario where multiple dynamic rows are created after confirming an inventory number from a MySQL database. However, I am facing an issue with inserting the inventory data only to the next DIV eleme ...