├── .github └── workflows │ └── publish.yml ├── package.json ├── test.js ├── main.js ├── LICENSE ├── .gitignore ├── README.md └── index.js /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 12 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-electron", 3 | "version": "0.0.10", 4 | "description": "Use Puppeteer's API with Electron", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "author": "nondanee", 10 | "license": "MIT", 11 | "dependencies": { 12 | "electron": "^9.1.0", 13 | "puppeteer-core": "^1.18.1" 14 | }, 15 | "keywords": [ 16 | "puppeteer", 17 | "electron" 18 | ], 19 | "repository": "nondanee/puppeteer-electron" 20 | } 21 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('.') 2 | 3 | ;(async () => { 4 | const app = await puppeteer.launch({ args: ['--autoplay-policy=no-user-gesture-required'] }) 5 | const pages = await app.pages() 6 | const [page] = pages 7 | 8 | const close = async () => await app.close() 9 | await page.exposeFunction('close', close) 10 | const audioHandle = await page.evaluateHandle(src => (new Audio(src)), 'https://files.catbox.moe/5955ob.m4a') 11 | await page.evaluate(audio => audio.onended = close, audioHandle) 12 | await page.evaluate(audio => audio.play(), audioHandle) 13 | })() -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require('electron') 2 | 3 | let options = {} 4 | try { options = JSON.parse(process.argv.pop()) } catch (error) {} 5 | 6 | Array.from(options.args || []) 7 | .filter(line => !line.includes('remote-debugging-port')) 8 | .forEach(line => 9 | app.commandLine.appendSwitch.apply(null, line.replace(/^--/, '').split('=')) 10 | ) 11 | 12 | app.once('ready', () => { 13 | const window = new BrowserWindow({ show: false }) 14 | window.loadURL('data:text/html') 15 | window.once('ready-to-show', () => { 16 | if (!options.headless) window.show() 17 | process.stdout.write('ready') 18 | }) 19 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nzix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .vscode 3 | .idea 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Puppeteer-Electron 2 | 3 | 4 | 5 | > A version of Puppeteer that use Electron instead of Chromium 6 | 7 | ⚠️ BEWARE: Experimental. Just for test. Can not work with all Puppeteer APIs. 8 | 9 | ## Motivation 10 | 11 | In comparison with the full-featured Chromium browser (~108MB Mac, ~113MB Linux, ~141MB Win for ZIP package), A portable alternative ------ Electron is able to handle most daily tasks but has half of Chromium's size (~55MB Mac, ~63MB Linux, ~58MB Win for ZIP package) 12 | 13 | ## Usage 14 | 15 | ```javascript 16 | const puppeteer = require('puppeteer-electron') 17 | 18 | ;(async () => { 19 | const app = await puppeteer.launch({ headless: false }) // default is true 20 | const pages = await app.pages() 21 | const [page] = pages 22 | await page.goto('https://bing.com') 23 | 24 | setTimeout(async () => await app.close(), 5000) 25 | })() 26 | ``` 27 | 28 | ## Reference 29 | 30 | - https://discuss.atom.io/t/solved-control-automate-an-electron-application-with-puppeteer/64126 31 | - https://stackoverflow.com/questions/51847667/how-to-automate-electronjs-app 32 | - https://github.com/peterdanis/electron-puppeteer-demo 33 | - https://github.com/electron/electron/issues/3331 34 | - https://github.com/electron/electron/issues/11515 35 | 36 | ## License 37 | 38 | The MIT License -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const path = require('path') 3 | const { spawn } = require('child_process') 4 | 5 | const electron = require('electron') 6 | const puppeteer = require('puppeteer-core') 7 | 8 | let child = null 9 | const electronPath = typeof (electron) === 'string' ? electron : electron.app.getPath('exe') 10 | 11 | const launch = options => { 12 | options = options || {} 13 | const env = Object.assign({}, options.env || process.env) 14 | if ('ELECTRON_RUN_AS_NODE' in env) delete env.ELECTRON_RUN_AS_NODE 15 | if (!('headless' in options)) options.headless = true 16 | const args = [path.join(__dirname, 'main.js'), '--remote-debugging-port=8315', JSON.stringify(options)] 17 | return new Promise(resolve => { 18 | const listener = data => data.toString() === 'ready' && resolve(child.stdout.off('data', listener)) 19 | child = spawn(electronPath, args, { env }) 20 | child.stdout.on('data', listener) 21 | }) 22 | } 23 | 24 | /* 25 | const endpoint = () => 26 | Promise.resolve() 27 | .then(() => new Promise((resolve, reject) => { 28 | http.request('http://localhost:8315/json/version') 29 | .on('response', response => resolve(response)) 30 | .on('error', error => reject(error)) 31 | .end() 32 | })) 33 | .then(response => new Promise((resolve, reject) => { 34 | const chunks = [] 35 | response 36 | .on('data', chunk => chunks.push(chunk)) 37 | .on('end', () => resolve(Buffer.concat(chunks))) 38 | .on('error', error => reject(error)) 39 | })) 40 | .then(body => JSON.parse(body)) 41 | .then(data => data.webSocketDebuggerUrl) 42 | */ 43 | 44 | const patch = browser => 45 | new Proxy(browser, { 46 | get: (target, key, receiver) => 47 | key === 'close' 48 | ? () => target.close().then(() => child && child.kill()) 49 | : Reflect.get(target, key, receiver), 50 | set: (target, key, value, receiver) => 51 | Reflect.set(target, key, value, receiver) 52 | }) 53 | 54 | module.exports.launch = options => { 55 | const { slowMo, defaultViewport } = options || {} 56 | return Promise.resolve() 57 | .then(() => launch(options)) 58 | // .then(() => endpoint()) 59 | // .then(browserWSEndpoint => puppeteer.connect({ browserWSEndpoint, slowMo, defaultViewport })) 60 | .then(() => puppeteer.connect({ browserURL: 'http://localhost:8315', slowMo, defaultViewport })) 61 | .then(browser => patch(browser)) 62 | } --------------------------------------------------------------------------------